Files
otaman/frontend/index.html
T
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

6195 lines
289 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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">
<link rel="icon" type="image/svg+xml" href="favicon.svg">
<link rel="icon" type="image/png" href="sim.png" sizes="462x462">
<link rel="manifest" href="manifest.json">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="apple-touch-icon" href="icon-192.png">
<script src="des-bundle.js"></script>
<script src="aes-bundle.js"></script>
</head>
<body class="bg-neutral-50 dark:bg-slate-900 text-gray-800 dark:text-slate-200">
<div class="max-w-7xl mx-auto px-6 py-2">
<div class="flex items-center justify-between mb-3">
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v1.9.9</span></h1>
<div class="flex items-center gap-4">
<button id="install-btn" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700" style="display:none">INSTALL PWA [for offline use]</button>
<a href="https://github.com/anttro/otaman" target="_blank" class="text-xs text-gray-400 hover:text-gray-600 dark:text-slate-500 dark:hover:text-slate-300">github</a>
<a id="help-link" href="help.html" target="_blank" rel="noopener" class="text-xs text-gray-400 hover:text-gray-600 dark:text-slate-500 dark:hover:text-slate-300" data-l10n="help">help</a>
<button onclick="toggleLang()" id="lang-btn" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700">EN</button>
<button onclick="toggleTheme()" id="theme-btn" class="px-2 py-1 text-sm rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700">🌙</button>
</div>
</div>
<div class="flex gap-1 mb-4 border-b border-gray-300 dark:border-slate-600">
<button class="tab-btn active px-4 py-1.5 text-sm rounded-t bg-blue-600 dark:bg-blue-500 text-white dark:text-white" data-tab="c-apdu">C-APDU</button>
<button class="tab-btn px-4 py-1.5 text-sm rounded-t bg-gray-200 dark:bg-slate-700 hover:bg-gray-300 dark:hover:bg-slate-600 text-gray-700 dark:text-slate-300" data-tab="scp80">SCP80</button>
<button class="tab-btn px-4 py-1.5 text-sm rounded-t bg-gray-200 dark:bg-slate-700 hover:bg-gray-300 dark:hover:bg-slate-600 text-gray-700 dark:text-slate-300" data-tab="response" data-l10n="Response parser">Response parser</button>
<button class="tab-btn px-4 py-1.5 text-sm rounded-t bg-gray-200 dark:bg-slate-700 hover:bg-gray-300 dark:hover:bg-slate-600 text-gray-700 dark:text-slate-300" data-tab="pysim" data-l10n="Card reader">Card reader</button>
</div>
<div id="tab-c-apdu" class="tab-content">
<div class="flex gap-4">
<div class="flex-1">
<div class="flex justify-center gap-2 mb-3">
<button class="c-apdu-subtab px-4 py-1.5 text-sm rounded-full bg-blue-600 text-white" data-sub="sim" onclick="cApduSwitchSubtab('sim')">SIM RFM</button>
<button class="c-apdu-subtab px-4 py-1.5 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-sub="usim" onclick="cApduSwitchSubtab('usim')">USIM RFM</button>
<button class="c-apdu-subtab px-4 py-1.5 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-sub="ber" onclick="cApduSwitchSubtab('ber')">Expanded Script</button>
<button class="c-apdu-subtab px-4 py-1.5 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-sub="ram" onclick="cApduSwitchSubtab('ram')">RAM/GP</button>
<button class="c-apdu-subtab px-4 py-1.5 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-sub="parse" onclick="cApduSwitchSubtab('parse')" data-l10n="C-APDU Parser">C-APDU Parser</button>
</div>
<div id="c-apdu-sub-sim">
<div class="flex gap-4 mb-3">
<div class="w-full">
<div class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Command">Command</label>
<select id="sim-cmd" onchange="updateSimUsimFields('sim')" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="select">SELECT</option>
<option value="update-record">UPDATE RECORD</option>
<option value="update-binary">UPDATE BINARY</option>
<option value="read-record">READ RECORD</option>
<option value="read-binary">READ BINARY</option>
<option value="erase-binary">ERASE BINARY</option>
<option value="activate-file">ACTIVATE FILE</option>
<option value="deactivate-file">DEACTIVATE FILE</option>
<option value="verify">VERIFY PIN</option>
<option value="change">CHANGE PIN</option>
<option value="disable">DISABLE PIN</option>
<option value="enable">ENABLE PIN</option>
<option value="unblock">UNBLOCK PIN</option>
</select>
</div>
<div id="sim-select-section">
<div id="sim-select-checkbox-row" class="mb-3">
<label class="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-slate-300">
<input id="sim-select" type="checkbox" checked class="rounded border-gray-300 dark:border-slate-600 dark:bg-slate-800" onchange="updateSimSelectSection('sim')">
Start with SELECT
</label>
</div>
<div id="sim-select-method-body">
<div id="sim-sel-method-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="SELECT method">SELECT method</label>
<select id="sim-sel-method" onchange="updateSimSelectMethod('sim'); updateP1P2Display('sim')" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="fid">By FID</option>
<option value="path">By full path from MF</option>
<option value="dfname">By DF name / AID</option>
<option value="chain">ADF RFM chain</option>
</select>
</div>
<div id="sim-sel-fid-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">FID (2 bytes)</label>
<input id="sim-sel-fid" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="6FC5" maxlength="4">
</div>
<div id="sim-sel-path-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Path (hex)">Path (hex)</label>
<input id="sim-sel-path" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="7FFF6FC5">
</div>
<div id="sim-sel-dfname-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">AID (hex)</label>
<input id="sim-sel-dfname" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="A000000151000000">
</div>
<div id="sim-sel-chain-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="FID chain (comma-separated)">FID chain (comma-separated)</label>
<input id="sim-sel-chain" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="3F00,2FE2,C0">
</div>
</div>
</div>
<div id="sim-record-row" class="mb-3">
<div class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Record #">Record #</label>
<input id="sim-record" type="number" min="1" max="255" value="1" onchange="updateP1P2Display('sim')" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
</div>
<div class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Selection mode (P2)">Selection mode (P2)</label>
<select id="sim-rec-mode" onchange="updateP1P2Display('sim')" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="04">Absolute (04)</option>
<option value="02">Next (02)</option>
<option value="03">Previous (03)</option>
</select>
</div>
</div>
<div id="sim-offset-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Offset (hex)">Offset (hex)</label>
<input id="sim-offset" onchange="updateP1P2Display('sim')" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="0000" maxlength="4" value="0000">
</div>
<div id="sim-act-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Target">Target</label>
<select id="sim-act-target" onchange="updateActFileInput('sim')" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="current" data-l10n="Current EF (case 1)">Current EF (case 1)</option>
<option value="fid" data-l10n="By FID">By FID</option>
<option value="mfpath" data-l10n="Path from MF">Path from MF</option>
<option value="dfpath" data-l10n="Path from current DF">Path from current DF</option>
</select>
<input id="sim-act-file" class="mt-1.5 font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="6FC5 / 7FFF6FC5" style="display:none">
</div>
<div id="sim-le-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Le (hex)</label>
<input id="sim-le" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="00" maxlength="2" value="00">
</div>
<div id="sim-data-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Data (hex)">Data (hex)</label>
<textarea id="sim-data" rows="3" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800"></textarea>
</div>
<div id="sim-recsize-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Record size (bytes)">Record size (bytes)</label>
<input id="sim-recsize" type="number" min="1" value="0" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
</div>
<div id="sim-pin-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">PIN</label>
<select id="sim-pin" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="01">PIN1 (01)</option>
<option value="02">PIN2 (02)</option>
</select>
</div>
<div id="sim-pin-val-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">PIN value</label>
<input id="sim-pin-val" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="1234" maxlength="8">
</div>
<div id="sim-pin-old-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Old PIN</label>
<input id="sim-pin-old" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="1234" maxlength="8">
</div>
<div id="sim-pin-new-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">New PIN</label>
<input id="sim-pin-new" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="5678" maxlength="8">
</div>
<div id="sim-override-row" class="mb-3">
<div class="flex gap-2 mb-2">
<div class="flex-1">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">P1 (hex)</label>
<input id="sim-p1" disabled class="font-mono disabled:opacity-50 disabled:cursor-not-allowed border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="00" maxlength="2" value="00">
</div>
<div class="flex-1">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">P2 (hex)</label>
<input id="sim-p2" disabled class="font-mono disabled:opacity-50 disabled:cursor-not-allowed border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="00" maxlength="2" value="00">
</div>
</div>
<label class="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-slate-300">
<input id="sim-override" type="checkbox" class="rounded border-gray-300 dark:border-slate-600 dark:bg-slate-800" onchange="toggleSimOverride('sim')">
Allow P1/P2 editing
</label>
</div>
</div>
</div>
<button onclick="genSimUsim('sim')" class="mb-3 px-5 py-2.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700" data-l10n="Generate APDU">Generate APDU</button>
<textarea id="sim-result" rows="3" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 bg-gray-100 dark:bg-slate-800"></textarea>
<button onclick="packToSp('sim-result')" id="sim-pack-btn" disabled class="mb-3 px-5 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Pack into Secured packet">Pack into Secured packet</button>
</div>
<div id="c-apdu-sub-usim" class="hidden">
<div class="flex gap-4 mb-3">
<div class="w-full">
<div class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Command">Command</label>
<select id="usim-cmd" onchange="updateSimUsimFields('usim')" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="select">SELECT</option>
<option value="update-record">UPDATE RECORD</option>
<option value="update-binary">UPDATE BINARY</option>
<option value="read-record">READ RECORD</option>
<option value="read-binary">READ BINARY</option>
<option value="activate-file">ACTIVATE FILE</option>
<option value="deactivate-file">DEACTIVATE FILE</option>
<option value="verify">VERIFY PIN</option>
<option value="change">CHANGE PIN</option>
<option value="disable">DISABLE PIN</option>
<option value="enable">ENABLE PIN</option>
<option value="unblock">UNBLOCK PIN</option>
</select>
</div>
<div id="usim-select-section">
<div id="usim-select-checkbox-row" class="mb-3">
<label class="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-slate-300">
<input id="usim-select" type="checkbox" checked class="rounded border-gray-300 dark:border-slate-600 dark:bg-slate-800" onchange="updateSimSelectSection('usim')">
Start with SELECT
</label>
</div>
<div id="usim-select-method-body">
<div id="usim-sel-method-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="SELECT method">SELECT method</label>
<select id="usim-sel-method" onchange="updateSimSelectMethod('usim'); updateP1P2Display('usim')" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="fid">By FID</option>
<option value="path">By full path from MF</option>
<option value="dfname">By DF name / AID</option>
<option value="chain">ADF RFM chain</option>
</select>
</div>
<div id="usim-sel-silent-row" class="mb-3">
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-slate-300">
<input id="usim-sel-silent" type="checkbox" onchange="updateP1P2Display('usim')" class="rounded border-gray-300 dark:border-slate-600 dark:bg-slate-800">
Silent (P2=0C, no FCP returned)
</label>
</div>
<div id="usim-sel-fid-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">FID (2 bytes)</label>
<input id="usim-sel-fid" oninput="spInvalidate()" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="6FC5" maxlength="4">
</div>
<div id="usim-sel-path-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Path (hex)">Path (hex)</label>
<input id="usim-sel-path" oninput="spInvalidate()" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="7FFF6FC5">
<div class="flex gap-4 mt-1.5 text-sm text-gray-700 dark:text-slate-300">
<label class="flex items-center gap-1.5"><input type="radio" name="usim-sel-base" value="mf" checked onchange="updateP1P2Display('usim')" class="rounded border-gray-300 dark:border-slate-600 dark:bg-slate-800"> from MF</label>
<label class="flex items-center gap-1.5"><input type="radio" name="usim-sel-base" value="df" onchange="updateP1P2Display('usim')" class="rounded border-gray-300 dark:border-slate-600 dark:bg-slate-800"> from current DF</label>
</div>
</div>
<div id="usim-sel-dfname-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">AID (hex)</label>
<input id="usim-sel-dfname" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="A000000151000000">
</div>
<div id="usim-sel-chain-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="FID chain (comma-separated)">FID chain (comma-separated)</label>
<input id="usim-sel-chain" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="3F00,2FE2,C0">
</div>
</div>
</div>
<div id="usim-record-row" class="mb-3">
<div class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Record #">Record #</label>
<input id="usim-record" type="number" min="1" max="255" value="1" onchange="updateP1P2Display('usim')" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
</div>
<div class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Selection mode (P2)">Selection mode (P2)</label>
<select id="usim-rec-mode" onchange="updateP1P2Display('usim')" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="04">Absolute (04)</option>
<option value="02">Next (02)</option>
<option value="03">Previous (03)</option>
</select>
</div>
</div>
<div id="usim-offset-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Offset (hex)">Offset (hex)</label>
<input id="usim-offset" onchange="updateP1P2Display('usim')" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="0000" maxlength="4" value="0000">
</div>
<div id="usim-act-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Target">Target</label>
<select id="usim-act-target" onchange="updateActFileInput('usim')" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="current" data-l10n="Current EF (case 1)">Current EF (case 1)</option>
<option value="fid" data-l10n="By FID">By FID</option>
<option value="mfpath" data-l10n="Path from MF">Path from MF</option>
<option value="dfpath" data-l10n="Path from current DF">Path from current DF</option>
</select>
<input id="usim-act-file" class="mt-1.5 font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="6FC5 / 7FFF6FC5" style="display:none">
</div>
<div id="usim-le-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Le (hex)</label>
<input id="usim-le" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="00" maxlength="2" value="00">
</div>
<div id="usim-data-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Data (hex)">Data (hex)</label>
<textarea id="usim-data" rows="3" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800"></textarea>
</div>
<div id="usim-recsize-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Record size (bytes)">Record size (bytes)</label>
<input id="usim-recsize" type="number" min="1" value="0" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
</div>
<div id="usim-pin-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">PIN</label>
<select id="usim-pin" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="01">PIN1 (01)</option>
<option value="02">PIN2 (02)</option>
</select>
</div>
<div id="usim-pin-val-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">PIN value</label>
<input id="usim-pin-val" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="1234" maxlength="8">
</div>
<div id="usim-pin-old-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Old PIN</label>
<input id="usim-pin-old" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="1234" maxlength="8">
</div>
<div id="usim-pin-new-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">New PIN</label>
<input id="usim-pin-new" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="5678" maxlength="8">
</div>
<div id="usim-override-row" class="mb-3">
<div class="flex gap-2 mb-2">
<div class="flex-1">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">P1 (hex)</label>
<input id="usim-p1" disabled class="font-mono disabled:opacity-50 disabled:cursor-not-allowed border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="00" maxlength="2" value="00">
</div>
<div class="flex-1">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">P2 (hex)</label>
<input id="usim-p2" disabled class="font-mono disabled:opacity-50 disabled:cursor-not-allowed border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="00" maxlength="2" value="00">
</div>
</div>
<label class="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-slate-300">
<input id="usim-override" type="checkbox" class="rounded border-gray-300 dark:border-slate-600 dark:bg-slate-800" onchange="toggleSimOverride('usim')">
Allow P1/P2 editing
</label>
</div>
</div>
</div>
<button onclick="genSimUsim('usim')" class="mb-3 px-5 py-2.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700" data-l10n="Generate APDU">Generate APDU</button>
<textarea id="usim-result" rows="3" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 bg-gray-100 dark:bg-slate-800"></textarea>
<button onclick="packToSp('usim-result')" id="usim-pack-btn" disabled class="mb-3 px-5 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Pack into Secured packet">Pack into Secured packet</button>
</div>
<div id="c-apdu-sub-ber" class="hidden">
<div class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Format</label>
<select id="ber-format" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 dark:text-slate-300 font-mono">
<option value="AA">Definite (AA)</option>
<option value="AE">Indefinite (AE)</option>
</select>
</div>
<div class="mb-3">
<div class="flex flex-wrap gap-1 mb-2 items-center">
<span class="text-sm font-medium text-gray-700 dark:text-slate-300 mr-1">Commands</span>
<button onclick="addBerRow('c-apdu')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ C-APDU</button>
<button onclick="addBerRow('immediate')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ Immediate</button>
<button onclick="addBerRow('error')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ Error</button>
<button onclick="addBerRow('chaining')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ Chaining</button>
</div>
<div id="ber-rows" class="space-y-2"></div>
</div>
<button onclick="genBerTlv()" class="mb-3 px-5 py-2.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700" data-l10n="Generate Expanded Script APDU">Generate Expanded Script APDU</button>
<textarea id="ber-result" rows="3" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 bg-gray-100 dark:bg-slate-800 dark:text-slate-300"></textarea>
<button onclick="packToSp('ber-result')" id="ber-pack-btn" disabled class="mb-3 px-5 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Pack into Secured packet">Pack into Secured packet</button>
</div>
<div id="c-apdu-sub-ram" class="hidden">
<div class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Command">Command</label>
<select id="ram-cmd" onchange="updateRamFields()" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="install-load">INSTALL [for load]</option>
<option value="install-install">INSTALL [for install]</option>
<option value="install-make-sel">INSTALL [for make selectable]</option>
<option value="install-reg-update">INSTALL [for registry update]</option>
<option value="install-extradition">INSTALL [for extradition]</option>
<option value="load">LOAD</option>
<option value="delete">DELETE</option>
<option value="get-status">GET STATUS</option>
<option value="get-data">GET DATA</option>
<option value="store-data">STORE DATA</option>
<option value="set-status">SET STATUS</option>
<option value="ext-auth">EXTERNAL AUTHENTICATE</option>
<option value="int-auth">INTERNAL AUTHENTICATE</option>
</select>
</div>
<div class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">AID (hex)</label>
<input id="ram-aid" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="A000000151000000">
</div>
<div id="ram-sdaid-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">SD AID (hex)</label>
<input id="ram-sdaid" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="A000000151000000">
</div>
<div id="ram-elfaid-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">ELF AID (hex)</label>
<input id="ram-elfaid" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="A000000151000000">
</div>
<div id="ram-modaid-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Module AID (hex)</label>
<input id="ram-modaid" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="A000000151000000">
</div>
<div id="ram-priv-row" class="mb-3">
<div class="flex items-center gap-3 mb-2">
<label class="text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Privileges">Privileges</label>
<input id="ram-priv" readonly class="font-mono w-20 border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1 bg-gray-100 dark:bg-slate-800" value="00">
</div>
<div class="grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
<div class="font-medium text-gray-700 dark:text-slate-300 mb-1" data-l10n="Byte 1">Byte 1</div>
<div class="font-medium text-gray-700 dark:text-slate-300 mb-1" data-l10n="Byte 2">Byte 2</div>
<label class="flex items-center gap-1"><input type="checkbox" class="ram-priv-b1" value="80" onchange="updateRamPriv()"> Security Domain</label>
<label class="flex items-center gap-1"><input type="checkbox" class="ram-priv-b2" value="80" onchange="updateRamPriv()"> Trusted Path</label>
<label class="flex items-center gap-1"><input type="checkbox" class="ram-priv-b1" value="40" onchange="updateRamPriv()"> DAP Verification</label>
<label class="flex items-center gap-1"><input type="checkbox" class="ram-priv-b2" value="40" onchange="updateRamPriv()"> Authorized Management</label>
<label class="flex items-center gap-1"><input type="checkbox" class="ram-priv-b1" value="20" onchange="updateRamPriv()"> Delegated Management</label>
<label class="flex items-center gap-1"><input type="checkbox" class="ram-priv-b2" value="20" onchange="updateRamPriv()"> Token Verification</label>
<label class="flex items-center gap-1"><input type="checkbox" class="ram-priv-b1" value="10" onchange="updateRamPriv()"> Card Lock</label>
<label class="flex items-center gap-1"><input type="checkbox" class="ram-priv-b2" value="10" onchange="updateRamPriv()"> Global Delete</label>
<label class="flex items-center gap-1"><input type="checkbox" class="ram-priv-b1" value="08" onchange="updateRamPriv()"> Card Terminate</label>
<label class="flex items-center gap-1"><input type="checkbox" class="ram-priv-b2" value="08" onchange="updateRamPriv()"> Global Lock</label>
<label class="flex items-center gap-1"><input type="checkbox" class="ram-priv-b1" value="04" onchange="updateRamPriv()"> Card Reset</label>
<label class="flex items-center gap-1"><input type="checkbox" class="ram-priv-b2" value="04" onchange="updateRamPriv()"> Global Registry</label>
<label class="flex items-center gap-1"><input type="checkbox" class="ram-priv-b1" value="02" onchange="updateRamPriv()"> CVM Management</label>
<label class="flex items-center gap-1"><input type="checkbox" class="ram-priv-b1" value="01" onchange="updateRamPriv()"> Mandated DAP Verification</label>
<label class="flex items-center gap-1"><input type="checkbox" class="ram-priv-b2" value="02" onchange="updateRamPriv()"> Final Application</label>
<div></div>
<label class="flex items-center gap-1"><input type="checkbox" class="ram-priv-b3" value="80" onchange="updateRamPriv()"> Receipt Generation</label>
</div>
</div>
<div id="ram-toolkit-row" class="mb-3">
<div class="mb-2">
<label class="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-slate-300">
<input id="ram-toolkit-enable" type="checkbox" class="rounded border-gray-300 dark:border-slate-600 dark:bg-slate-800" onchange="updateRamToolkit()">
SIM/UICC Toolkit parameters
</label>
</div>
<div id="ram-toolkit-body" class="">
<div class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Toolkit mode">Toolkit mode</label>
<select id="ram-tk-mode" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="ca">SIM Toolkit (Tag CA)</option>
<option value="ea">UICC Toolkit (Tag EA)</option>
</select>
</div>
<div class="grid grid-cols-2 gap-x-3 gap-y-2 text-sm">
<div>
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Priority">Priority</label>
<input id="ram-tk-priority" type="number" min="0" max="255" value="0" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1.5 dark:bg-slate-800">
</div>
<div>
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Timers (max)">Timers (max)</label>
<input id="ram-tk-timers" type="number" min="0" max="255" value="0" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1.5 dark:bg-slate-800">
</div>
<div>
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Menu text length">Menu text length</label>
<input id="ram-tk-textlen" type="number" min="0" max="255" value="0" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1.5 dark:bg-slate-800">
</div>
<div>
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Menu items">Menu items</label>
<input id="ram-tk-menus" type="number" min="0" max="255" value="0" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1.5 dark:bg-slate-800">
</div>
<div>
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="First menu position">First menu position</label>
<input id="ram-tk-firstpos" type="number" min="0" max="255" value="0" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1.5 dark:bg-slate-800">
</div>
<div>
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="First menu ID (hex)">First menu ID (hex)</label>
<input id="ram-tk-firstid" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1.5 dark:bg-slate-800" placeholder="00" maxlength="2" value="00">
</div>
<div>
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Last menu position">Last menu position</label>
<input id="ram-tk-lastpos" type="number" min="0" max="255" value="0" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1.5 dark:bg-slate-800">
</div>
<div>
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Last menu ID (hex)">Last menu ID (hex)</label>
<input id="ram-tk-lastid" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1.5 dark:bg-slate-800" placeholder="00" maxlength="2" value="00">
</div>
<div>
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Channels (max)">Channels (max)</label>
<input id="ram-tk-channels" type="number" min="0" max="255" value="0" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1.5 dark:bg-slate-800">
</div>
<div>
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300">MSL (SPI1, hex)</label>
<select id="ram-tk-msl" class="w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1.5 dark:bg-slate-800">
<option value="00">00 — no verification</option>
<option value="11">11 — RC/CC/DS</option>
<option value="12">12 — RC/DS/CC</option>
<option value="15">15 — RC/DS/CC + MAC</option>
<option value="16" selected>16 — RC/DS/CC + MAC + Cipher</option>
<option value="19">19 — RC/DS/CC + MAC + Cipher + DS</option>
</select>
</div>
<div>
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300">TAR (3 bytes)</label>
<input id="ram-tk-tar" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1.5 dark:bg-slate-800" placeholder="B00001" maxlength="6" value="B00001">
</div>
<div id="ram-tk-ad-row">
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Access domain (hex)">Access domain (hex)</label>
<input id="ram-tk-ad" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1.5 dark:bg-slate-800" placeholder="00" maxlength="2" value="00">
</div>
<div id="ram-tk-services-row">
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Max services">Max services</label>
<input id="ram-tk-services" type="number" min="0" max="255" value="0" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1.5 dark:bg-slate-800">
</div>
</div>
</div>
</div>
<div id="ram-load-data-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Load data (hex)">Load data (hex)</label>
<textarea id="ram-load-data" rows="3" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800"></textarea>
</div>
<div id="ram-data-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Data (hex)">Data (hex)</label>
<textarea id="ram-data" rows="3" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800"></textarea>
</div>
<div id="ram-block-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Block number">Block number</label>
<input id="ram-block" type="number" min="0" value="0" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
</div>
<div id="ram-enc-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Encryption">Encryption</label>
<select id="ram-enc" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="00">No encryption, more blocks</option>
<option value="40">App-dependent encryption, more blocks</option>
<option value="80">No encryption, last block</option>
<option value="C0">App-dependent encryption, last block</option>
<option value="E0">With encryption, last block</option>
</select>
</div>
<div id="ram-authp1-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Security level (P1)</label>
<select id="ram-authp1" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="00">00 — No secure messaging</option>
<option value="01">01 — C-MAC</option>
<option value="03">03 — C-DEC + C-MAC</option>
</select>
</div>
<div id="ram-del-mode-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="DELETE mode">DELETE mode</label>
<select id="ram-del-mode" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="00">AID only (00)</option>
<option value="80">AID and all related objects (80)</option>
</select>
</div>
<div id="ram-gs-mode-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="GET STATUS mode">GET STATUS mode</label>
<select id="ram-gs-mode" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="80">ISD (80)</option>
<option value="40">Applications and SSD (40)</option>
<option value="20">Executable Load Files (20)</option>
<option value="10">ELF and modules (10)</option>
</select>
</div>
<div id="ram-gs-p2-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="GET STATUS format">GET STATUS format</label>
<select id="ram-gs-p2" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="02">02 — First/all, TLV format</option>
<option value="03">03 — Next, TLV format</option>
<option value="00">00 — First/all, raw format (deprecated)</option>
</select>
</div>
<div id="ram-tag-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="GET DATA tag">GET DATA tag</label>
<select id="ram-gd-tag-sel" onchange="document.getElementById('ram-tag').value=this.value" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 mb-1.5">
<option value="" data-l10n="— select preset —">— select preset —</option>
<option value="42">42 — Issuer Identification Number (IIN)</option>
<option value="45">45 — Card Image Number (CIN)</option>
<option value="66">66 — Card Data / SD Management Data</option>
<option value="67">67 — Card Capability Information</option>
<option value="E0">E0 — Key Information Template</option>
<option value="D3">D3 — Current Security Level</option>
<option value="2F00">2F00 — List of Applications</option>
<option value="FF21">FF21 — Extended Card Resources Info</option>
<option value="5F50">5F50 — SD Manager URL</option>
<option value="C1">C1 — Sequence Counter (SCP02/03)</option>
<option value="C2">C2 — Confirmation Counter</option>
<option value="7F21">7F21 — Certificate (SD public key)</option>
<option value="5031">5031 — Certificate info (EF.OD)</option>
</select>
<input id="ram-tag" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="5F50" maxlength="4" value="5F50">
</div>
<div id="ram-ss-mode-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="SET STATUS type">SET STATUS type</label>
<select id="ram-ss-mode" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="80">ISD (80)</option>
<option value="40">Application or SSD (40)</option>
<option value="60">SD + associated apps (60)</option>
</select>
</div>
<div id="ram-ss-state-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="State">State</label>
<select id="ram-ss-state" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="00">00 — Unlock (previous state)</option>
<option value="80">80 — Lock (LOCKED)</option>
<option value="01">01 — OP_READY</option>
<option value="07">07 — INITIALIZED</option>
<option value="0F">0F — SECURED</option>
<option value="7F">7F — CARD_LOCKED</option>
<option value="FF">FF — TERMINATED</option>
</select>
</div>
<button onclick="genRam()" class="mb-3 px-5 py-2.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700" data-l10n="Generate APDU">Generate APDU</button>
<textarea id="ram-result" rows="3" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 bg-gray-100 dark:bg-slate-800"></textarea>
<button onclick="packToSp('ram-result')" id="ram-pack-btn" disabled class="mb-3 px-5 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Pack into Secured packet">Pack into Secured packet</button>
</div>
<div id="c-apdu-sub-parse" class="hidden">
<div class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="APDU hex">APDU hex</label>
<textarea id="parse-input" rows="4" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="80 E6 0C 00 ..."></textarea>
</div>
<button onclick="parseHex()" class="mb-3 px-5 py-2.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700" data-l10n="Decode">Decode</button>
<div id="parse-output"></div>
</div>
</div>
<div class="w-1/4">
<div class="border border-gray-300 dark:border-slate-600 rounded p-3 bg-gray-50 dark:bg-slate-800">
<select id="conv-type" onchange="updateConvFields()" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 mb-2">
<option value="nibswap">Nibble swap</option>
<option value="imsi">IMSI → EF.IMSI</option>
<option value="msisdn">MSISDN → BCD</option>
<option value="iccid">ICCID → hex</option>
<option value="spn">Provider Name → SPN</option>
<option value="plmn">MCC,MNC → PLMNsel</option>
<option value="plmnwact">MCC,MNC,AcT → PLMNwAcT</option>
</select>
<div class="flex flex-col gap-2 mb-2">
<input id="conv-input" class="font-mono flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800">
<button onclick="genConvert()" class="px-3 py-1.5 bg-blue-600 text-white text-xs font-medium rounded hover:bg-blue-700" data-l10n="Convert">Convert</button>
</div>
<div id="conv-plmn-fields" class="" style="display:none">
<div class="flex gap-2 mb-2">
<div class="flex-1">
<input id="conv-mcc" maxlength="3" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800" placeholder="MCC">
</div>
<div class="flex-1">
<input id="conv-mnc" maxlength="3" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800" placeholder="MNC">
</div>
</div>
<div id="conv-act-row" class="" style="display:none">
<select id="conv-act" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800">
<option value="8000">GSM (8000)</option>
<option value="4000">UTRAN (4000)</option>
<option value="2000">E-UTRAN (2000)</option>
<option value="1000">NGRAN (1000)</option>
<option value="C000">GSM + UTRAN (C000)</option>
<option value="6000">UTRAN + E-UTRAN (6000)</option>
<option value="E000">GSM + UTRAN + E-UTRAN (E000)</option>
<option value="F000">All (F000)</option>
</select>
</div>
</div>
<textarea id="conv-result" rows="3" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1.5 mt-2 bg-white dark:bg-slate-800"></textarea>
</div>
</div>
</div>
</div>
<div id="tab-scp80" class="tab-content hidden">
<div class="flex gap-2 mb-3">
<button class="scp80-subtab px-4 py-1.5 text-sm rounded-full bg-blue-600 text-white" data-scp80-sub="sp" onclick="scp80SwitchSubtab('sp')" data-l10n="Secured packet">Secured packet</button>
<button class="scp80-subtab px-4 py-1.5 text-sm rounded-full bg-gray-200 dark:bg-slate-700 hover:bg-gray-300 dark:hover:bg-slate-600 text-gray-700 dark:text-slate-300" data-scp80-sub="cards" onclick="scp80SwitchSubtab('cards')" data-l10n="Cards">Cards</button>
</div>
<div id="scp80-sub-sp">
<div class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">SPI1 (Security Parameter Indicator)</label>
<div class="flex gap-2 items-center">
<select id="sp-spi1" onchange="updateSp()" class="flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="00">00 — No security</option>
<option value="01">01 — RC only</option>
<option value="02">02 — CC/MAC only</option>
<option value="06" selected>06 — CC/MAC + Ciphering</option>
<option value="0A">0A — CC/MAC + Counter (available)</option>
<option value="0E">0E — CC/MAC + Ciphering + Counter (available)</option>
<option value="12">12 — CC/MAC + Counter (higher)</option>
<option value="16">16 — CC/MAC + Ciphering + Counter (higher)</option>
<option value="1A">1A — CC/MAC + Counter (+1)</option>
<option value="1E">1E — CC/MAC + Ciphering + Counter (+1)</option>
</select>
<input id="sp-spi1-hex" readonly class="w-16 font-mono border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-2 bg-gray-100 dark:bg-slate-800 text-center" value="06">
</div>
</div>
<div class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">SPI2 (PoR settings)</label>
<div class="flex gap-2 items-center">
<select id="sp-spi2" onchange="updateSp()" class="flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="00" selected>00 — No PoR, no security</option>
<option value="01">01 — PoR required, no security</option>
<option value="05">05 — PoR required, RC</option>
<option value="09">09 — PoR required, CC</option>
<option value="0D">0D — PoR required, DS</option>
<option value="11">11 — PoR required, no security, ciphered</option>
<option value="02">02 — PoR on error, no security</option>
<option value="06">06 — PoR on error, RC</option>
</select>
<select id="sp-spi2-sm" onchange="updateSp()" class="border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="0">DELIVER-REPORT</option>
<option value="1">SUBMIT SM</option>
</select>
<input id="sp-spi2-hex" readonly class="w-16 font-mono border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-2 bg-gray-100 dark:bg-slate-800 text-center" value="00">
</div>
</div>
<div class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">KIc (Ciphering Key Identifier)</label>
<div class="flex gap-2 items-center">
<select id="sp-kic-idx" onchange="updateSpKic()" class="border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="0">Implicit (0)</option>
<option value="1" selected>1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8">8</option>
<option value="9">9</option>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
<option value="D">D</option>
<option value="E">E</option>
<option value="F">F</option>
</select>
<select id="sp-kic-alg" onchange="updateSpKic()" class="flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="01">01 — DES-CBC (8B key)</option>
<option value="02">02 — AES-CBC (16/24/32B key)</option>
<option value="05" selected>05 — 3DES 2-key outer-CBC (16B key)</option>
<option value="09">09 — 3DES 3-key outer-CBC (24B key)</option>
<option value="0D">0D — DES-ECB (8B key)</option>
</select>
<input id="sp-kic-hex" readonly class="w-12 font-mono border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-2 bg-gray-100 dark:bg-slate-800 text-center" value="15">
</div>
</div>
<div class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">KID (MAC Key Identifier)</label>
<div class="flex gap-2 items-center">
<select id="sp-kid-idx" onchange="updateSpKid()" class="border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="0">Implicit (0)</option>
<option value="1" selected>1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8">8</option>
<option value="9">9</option>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
<option value="D">D</option>
<option value="E">E</option>
<option value="F">F</option>
</select>
<select id="sp-kid-alg" onchange="updateSpKid()" class="flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="01">01 — DES-CBC (8B key)</option>
<option value="02">02 — AES-CMAC (16/24/32B key)</option>
<option value="05" selected>05 — 3DES 2-key outer-CBC (16B key)</option>
<option value="09">09 — 3DES 3-key outer-CBC (24B key)</option>
</select>
<input id="sp-kid-hex" readonly class="w-12 font-mono border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-2 bg-gray-100 dark:bg-slate-800 text-center" value="15">
</div>
</div>
<div class="flex gap-2 mb-3">
<div class="flex-1">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">TAR (3 bytes)</label>
<input id="sp-tar" oninput="spInvalidate()" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="B00001" maxlength="6" value="B00001">
</div>
<div class="flex-1">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Card presets">Card presets</label>
<select id="sp-card-sel" onchange="cardsApply(this.value)" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="" data-l10n="— Select card —">— Select card —</option>
</select>
</div>
<div class="flex-1">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Counter (5 bytes)</label>
<div class="flex gap-1 items-center">
<button type="button" onclick="spCntrAdjust(-1)" class="px-2.5 py-2 border border-gray-300 dark:border-slate-600 text-sm rounded hover:bg-gray-200 dark:bg-slate-700 dark:hover:bg-slate-600"></button>
<input id="sp-cntr" oninput="spInvalidate()" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 flex-1 dark:bg-slate-800" placeholder="0000000001" maxlength="10" value="0000000001">
<button type="button" onclick="spCntrAdjust(1)" class="px-2.5 py-2 border border-gray-300 dark:border-slate-600 text-sm rounded hover:bg-gray-200 dark:bg-slate-700 dark:hover:bg-slate-600">+</button>
</div>
</div>
</div>
<div class="flex gap-2 mb-3">
<div class="flex-1">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">KIc Key (hex)</label>
<textarea id="sp-kic-key" rows="2" oninput="spInvalidate()" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="16 bytes key"></textarea>
</div>
<div class="flex-1">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">KID Key (hex)</label>
<textarea id="sp-kid-key" rows="2" oninput="spInvalidate()" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="16 bytes key"></textarea>
</div>
</div>
<div class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Padding byte</label>
<select id="sp-padding" onchange="spInvalidate()" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="00" selected>00 (TS 102 225 default)</option>
<option value="FF">FF</option>
</select>
</div>
<div class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">APDU (hex)</label>
<textarea id="sp-apdu" rows="3" oninput="spInvalidate()" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800"></textarea>
</div>
<button onclick="genSp()" class="mb-3 px-5 py-2.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700" data-l10n="Generate Secure Packet">Generate Secure Packet</button>
<button onclick="pysimVerifySp()" id="sp-verify-btn" class="hidden mb-3 px-5 py-2.5 bg-slate-600 text-white text-sm font-medium rounded hover:bg-slate-700" data-l10n="Verify vs pySim">Verify vs pySim</button>
<button onclick="pysimSendOta()" id="sp-send-btn" class="hidden mb-3 px-5 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700" data-l10n="Send to Card">Send to Card</button>
<div id="sp-por-status" class="mt-2 text-base font-semibold font-mono hidden"></div>
<textarea id="sp-result" rows="3" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 bg-gray-100 dark:bg-slate-800"></textarea>
<div id="sp-send-result" class="mt-2 text-xs font-mono hidden"></div>
<div id="sp-verify-result" class="mt-2 text-xs font-mono hidden"></div>
</div>
<div id="scp80-sub-cards" class="hidden">
<p class="text-xs text-gray-500 dark:text-slate-400 mb-3" data-l10n="Card presets are stored locally in your browser. JSON export/import buttons are below the card list.">Card presets are stored locally in your browser. JSON export/import buttons are below the card list.</p>
<div class="flex gap-2 mb-3">
<input id="cards-name" class="flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="Name (e.g. Foobar SIM)">
<input id="cards-iccid" class="flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 font-mono" placeholder="ICCID (optional)">
<button onclick="cardsAdd()" class="px-4 py-2.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700 whitespace-nowrap" data-l10n="Add">Add</button>
</div>
<div class="flex gap-2 mb-3 flex-wrap">
<input id="cards-kic" class="w-12 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono text-center" placeholder="KIc" maxlength="2">
<input id="cards-kid" class="w-12 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono text-center" placeholder="KID" maxlength="2">
<input id="cards-spi1" class="w-12 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono text-center" placeholder="SPI1" maxlength="2">
<input id="cards-spi2" class="w-12 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono text-center" placeholder="SPI2" maxlength="2">
<input id="cards-tar" class="w-20 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono" placeholder="ISD TAR" maxlength="6">
<input id="cards-cntr" class="w-24 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono" placeholder="Counter" maxlength="10">
</div>
<div class="flex gap-2 mb-3">
<input id="cards-kic-key" class="flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 font-mono" placeholder="KIc key (32 hex chars)">
<input id="cards-kid-key" class="flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 font-mono" placeholder="KID key (32 hex chars)">
</div>
<div id="cards-list" class="overflow-x-auto">
<table class="w-full text-sm">
<thead><tr>
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="Name">Name</th>
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="ICCID">ICCID</th>
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700">KIc</th>
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700">KID</th>
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700">SPI1</th>
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700">SPI2</th>
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700">TAR</th>
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="Cntr">Cntr</th>
<th class="border-b border-gray-200 dark:border-slate-700"></th>
</tr></thead>
<tbody id="cards-tbody"></tbody>
</table>
</div>
<input type="file" id="cards-file" accept=".json,application/json" class="hidden" onchange="cardsImportFile(this)">
<div class="flex flex-wrap gap-2 mt-2">
<button onclick="cardsExport()" class="px-2 py-1 text-xs rounded bg-blue-600 text-white hover:bg-blue-700" data-l10n="Export as JSON">Export as JSON</button>
<button onclick="cardsExportFile()" class="px-2 py-1 text-xs rounded bg-blue-600 text-white hover:bg-blue-700" data-l10n="Export to file">Export to file</button>
<button onclick="document.getElementById('cards-file').click()" class="px-2 py-1 text-xs rounded bg-gray-600 text-white hover:bg-gray-700" data-l10n="Import from file">Import from file</button>
<button onclick="cardsImportField()" class="px-2 py-1 text-xs rounded bg-gray-600 text-white hover:bg-gray-700" data-l10n="Paste & import">Paste &amp; import</button>
<button onclick="cardsImport()" class="px-2 py-1 text-xs rounded bg-gray-600 text-white hover:bg-gray-700" data-l10n="Import JSON from clipboard">Import JSON from clipboard</button>
</div>
<textarea id="cards-io" rows="4" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 hidden" placeholder="JSON data"></textarea>
</div>
</div>
<div id="tab-response" class="tab-content hidden">
<div class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Command">Command</label>
<select id="resp-cmd" onchange="updateRespCmd()" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="" data-l10n="— Select command —">— Select command —</option>
<optgroup label="SIM / USIM">
<option value="sim-select">SELECT</option>
<option value="sim-status">STATUS</option>
<option value="sim-read-binary">READ BINARY</option>
<option value="sim-update-binary">UPDATE BINARY</option>
<option value="sim-read-record">READ RECORD</option>
<option value="sim-update-record">UPDATE RECORD</option>
<option value="sim-verify">VERIFY PIN</option>
<option value="sim-change">CHANGE PIN</option>
<option value="sim-disable">DISABLE PIN</option>
<option value="sim-enable">ENABLE PIN</option>
<option value="sim-unblock">UNBLOCK PIN</option>
<option value="sim-deactivate">DEACTIVATE FILE</option>
<option value="sim-activate">ACTIVATE FILE</option>
<option value="sim-challenge">GET CHALLENGE</option>
<option value="sim-auth">AUTHENTICATE</option>
<option value="sim-profile">TERMINAL PROFILE</option>
<option value="sim-envelope">ENVELOPE</option>
<option value="sim-fetch">FETCH</option>
<option value="sim-terminal-rsp">TERMINAL RESPONSE</option>
<option value="sim-manage-channel">MANAGE CHANNEL</option>
<option value="sim-msc">MANAGE SECURE CHANNEL</option>
<option value="sim-retrieve">RETRIEVE DATA</option>
<option value="sim-set-data">SET DATA</option>
<option value="sim-suspend">SUSPEND UICC</option>
</optgroup>
<optgroup label="RAM (GlobalPlatform)">
<option value="ram-install-load">INSTALL [for load]</option>
<option value="ram-install-install">INSTALL [for install]</option>
<option value="ram-install-make-sel">INSTALL [for make selectable]</option>
<option value="ram-install-reg-update">INSTALL [for registry update]</option>
<option value="ram-install-extradition">INSTALL [for extradition]</option>
<option value="ram-load">LOAD</option>
<option value="ram-delete">DELETE</option>
<option value="ram-get-status">GET STATUS</option>
<option value="ram-get-data">GET DATA</option>
<option value="ram-store-data">STORE DATA</option>
<option value="ram-set-status">SET STATUS</option>
<option value="ram-ext-auth">EXTERNAL AUTHENTICATE</option>
<option value="ram-int-auth">INTERNAL AUTHENTICATE</option>
<option value="ram-select">SELECT</option>
<option value="ram-init-update">INITIALIZE UPDATE</option>
<option value="ram-put-key">PUT KEY</option>
<option value="ram-manage-channel">MANAGE CHANNEL</option>
</optgroup>
</select>
</div>
<div class="flex gap-3 mb-3">
<div class="w-24">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">SW</label>
<input id="resp-sw" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" maxlength="4" placeholder="9000">
</div>
<div class="flex-1">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Context</label>
<input id="resp-ctx" readonly class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 bg-gray-100 dark:bg-slate-800" value="generic">
</div>
</div>
<div class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Response data (hex)">Response data (hex)</label>
<textarea id="resp-data" rows="3" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="E3 1A 4F 08 A0 00 00 00 03 00 00 00 9F 70 01 07 C5 03 80 00 00"></textarea>
</div>
<button onclick="decodeResponse()" class="mb-3 px-5 py-2.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700" data-l10n="Decode">Decode</button>
<div id="resp-output" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 bg-gray-100 dark:bg-slate-800 whitespace-pre-wrap" style="min-height:100px"></div>
</div>
<div id="tab-pysim" class="tab-content hidden">
<div id="pysim-connect-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Server URL">Server URL</label>
<div class="flex gap-2 items-center">
<input id="pysim-url" class="font-mono flex-1 max-w-md border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800" value="http://127.0.0.1:8080">
<button onclick="pysimConnect()" id="pysim-connect-btn" class="px-4 py-2.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700" data-l10n="Connect">Connect</button>
</div>
</div>
<div id="pysim-connect-info" class="mb-3 text-xs text-gray-500 dark:text-slate-400 leading-relaxed">
<p data-l10n="To access local card reader, you need pysim-otaman-server running locally.">To access local card reader, you need pysim-otaman-server running locally.</p>
<p><span data-l10n="To connect, install">To connect, install</span> <a href="https://github.com/anttro/otaman" target="_blank" class="text-blue-600 dark:text-blue-400 underline">pysim-otaman-server</a>: <code class="font-mono bg-gray-100 dark:bg-slate-700 px-1 rounded">git clone https://github.com/anttro/otaman.git</code> <span data-l10n="and start it.">and start it.</span><br><span data-l10n="Refer to pysim-otaman-server README files for details.">Refer to pysim-otaman-server README files for details.</span> <span data-l10n="Click">Click</span> <b data-l10n="Connect">Connect</b> <span data-l10n="when pysim-otaman-server is ready">when pysim-otaman-server is ready</span></p>
<div id="pysim-connect-msg" class="text-xs"></div>
</div>
<div id="pysim-connected-row" class="mb-3 hidden">
<div class="flex gap-3 items-start">
<img id="pysim-status-img" src="nosim.svg" alt="pysim" class="w-16 h-16 shrink-0 dark:invert">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1 flex-wrap">
<button onclick="pysimEquipCmd()" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300" data-l10n="Equip card">Equip card</button>
<button onclick="pysimStatusCmd()" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300" data-l10n="Check status">Check status</button>
<button onclick="pysimResetCmd()" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300" data-l10n="Reset card">Reset card</button>
<button onclick="pysimDisconnect()" id="pysim-disconnect-btn" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300" data-l10n="Disconnect from pySim">Disconnect from pySim</button>
</div>
<div id="pysim-status" class="text-xs font-mono text-gray-600 dark:text-slate-400"></div>
</div>
</div>
</div>
<div id="pysim-body" class="hidden">
<div class="flex gap-1 mb-2">
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-blue-600 text-white" data-pysim-sub="files" data-l10n="File manager">File manager</button>
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="custom" data-l10n="Custom files">Custom files</button>
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="cmd" data-l10n="pySim command line">pySim command line</button>
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="apdu" data-l10n="Raw APDU">Raw APDU</button>
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="proactive">Proactive UICC</button>
<button id="stk-menu-btn" onclick="stkMenuOpen()" class="pysim-subtab px-3 py-1 text-sm rounded-full bg-emerald-600 text-white hover:bg-emerald-700" style="display:none">STK: <span id="stk-menu-title"></span></button>
</div>
<div id="pysim-sub-files">
<div class="flex gap-2">
<div class="w-1/3 border border-gray-300 dark:border-slate-600 rounded p-2 text-xs font-mono overflow-y-auto bg-gray-50 dark:bg-slate-800" style="max-height:420px">
<div id="pysim-fs-tree"></div>
</div>
<div class="flex-1">
<div id="pysim-fs-detail" class="hidden">
<div class="text-xs font-mono mb-1"><span data-l10n="File:">File:</span> <b id="pysim-fs-filename"></b></div>
<div id="pysim-fs-content" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1.5 bg-gray-100 dark:bg-slate-800 min-h-20 overflow-y-auto"></div>
<div class="flex gap-2 mt-1 items-center">
<button onclick="pysimFsRead()" id="pysim-fs-read-btn" class="px-2 py-1 text-xs rounded bg-blue-600 text-white hover:bg-blue-700" data-l10n="Read">Read</button>
<button onclick="pysimFsSave()" id="pysim-fs-save-btn" class="px-2 py-1 text-xs rounded bg-emerald-600 text-white hover:bg-emerald-700 hidden" data-l10n="Save">Save</button>
<button onclick="pysimFsCancel()" id="pysim-fs-cancel-btn" class="px-2 py-1 text-xs rounded bg-gray-600 text-white hover:bg-gray-700 hidden" data-l10n="Cancel">Cancel</button>
<span class="text-xs text-gray-400 dark:text-slate-500">|</span>
<span class="pysim-mode-pill px-2 py-0.5 text-xs rounded cursor-pointer bg-blue-600 text-white" data-mode="raw" onclick="pysimFsSetMode('raw')" data-l10n="Raw">Raw</span>
<span class="pysim-mode-pill px-2 py-0.5 text-xs rounded cursor-pointer bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300" data-mode="dec" onclick="pysimFsSetMode('dec')" data-l10n="Decoded">Decoded</span>
<button onclick="pysimFsEdit()" id="pysim-fs-edit-btn" class="px-2 py-1 text-xs rounded bg-blue-600 text-white hover:bg-blue-700 ml-auto" data-l10n="Edit">Edit</button>
</div>
<div id="pysim-fs-status" class="mt-1 text-xs font-mono text-gray-500 dark:text-slate-400" style="min-height:1.2em"></div>
</div>
</div>
</div>
</div>
<div id="pysim-sub-custom" class="hidden">
<div class="mb-2 text-xs font-mono text-gray-600 dark:text-slate-400" data-l10n="Add known file paths and aliases to the file tree.">Add known file paths and aliases to the file tree.</div>
<div class="flex gap-2 items-center mb-2">
<input id="pysim-cf-path" class="font-mono flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800" placeholder="3F00/6F46" onkeydown="if(event.key==='Enter')pysimCustomAdd()">
<input id="pysim-cf-name" class="font-mono w-48 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800" placeholder="EF.SPN" onkeydown="if(event.key==='Enter')pysimCustomAdd()">
<button onclick="pysimCustomAdd()" class="px-3 py-2.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700 whitespace-nowrap" data-l10n="Add">Add</button>
</div>
<div id="pysim-cf-list" class="text-xs font-mono text-gray-600 dark:text-slate-400"></div>
<input type="file" id="pysim-cf-file" accept=".json,application/json" class="hidden" onchange="pysimCustomImportFile(this)">
<div class="flex flex-wrap gap-2 mt-2">
<button onclick="pysimCustomExport()" class="px-2 py-1 text-xs rounded bg-blue-600 text-white hover:bg-blue-700" data-l10n="Export as JSON">Export as JSON</button>
<button onclick="pysimCustomExportFile()" class="px-2 py-1 text-xs rounded bg-blue-600 text-white hover:bg-blue-700" data-l10n="Export to file">Export to file</button>
<button onclick="document.getElementById('pysim-cf-file').click()" class="px-2 py-1 text-xs rounded bg-gray-600 text-white hover:bg-gray-700" data-l10n="Import from file">Import from file</button>
<button onclick="pysimCustomImportField()" class="px-2 py-1 text-xs rounded bg-gray-600 text-white hover:bg-gray-700" data-l10n="Paste & import">Paste &amp; import</button>
<button onclick="pysimCustomImport()" class="px-2 py-1 text-xs rounded bg-gray-600 text-white hover:bg-gray-700" data-l10n="Import JSON from clipboard">Import JSON from clipboard</button>
</div>
<textarea id="pysim-cf-io" class="hidden mt-2 w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1.5 bg-gray-100 dark:bg-slate-800" rows="6"></textarea>
</div>
<div id="pysim-sub-cmd" class="hidden">
<div class="flex gap-2 items-center mb-2">
<div class="relative flex-1">
<input id="pysim-cmd" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800" placeholder="select MF" autocomplete="off" onkeydown="pysimCmdKeydown(event)" oninput="pysimCmdInput()">
<div id="pysim-cmd-suggest" class="hidden absolute bottom-full left-0 right-0 mb-1 max-h-32 overflow-y-auto border border-gray-300 dark:border-slate-600 rounded bg-white dark:bg-slate-800 text-xs font-mono shadow-lg z-10"></div>
</div>
<button onclick="pysimExec()" class="px-4 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700" data-l10n="Execute">Execute</button>
</div>
<div id="pysim-cmd-hint" class="hidden mb-2 text-xs font-mono text-gray-500 dark:text-slate-400 border border-gray-200 dark:border-slate-700 rounded p-2 bg-gray-50 dark:bg-slate-800 whitespace-pre-wrap"></div>
<textarea id="pysim-cmd-output" rows="10" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 bg-gray-100 dark:bg-slate-800"></textarea>
</div>
<div id="pysim-sub-apdu" class="hidden">
<div class="flex gap-2 items-center mb-2">
<input id="pysim-apdu" class="font-mono flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800" placeholder="00A4040000" onkeydown="if(event.key==='Enter')pysimApdu()">
<button onclick="pysimApdu()" class="px-4 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700" data-l10n="Send">Send</button>
</div>
<textarea id="pysim-apdu-output" rows="5" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 bg-gray-100 dark:bg-slate-800"></textarea>
</div>
<div id="pysim-sub-proactive" class="hidden">
<div class="flex gap-6">
<div>
<div class="mb-1 text-sm text-gray-500 dark:text-slate-400" data-l10n="STATUS and Polling">STATUS and Polling</div>
<div class="flex items-center gap-2 mb-3">
<button id="pli-status-btn" onclick="pysimStatusPoll()" class="px-2.5 py-1 text-sm rounded bg-gray-600 text-white hover:bg-gray-700" data-l10n="Send STATUS">Send STATUS</button>
<span class="text-xs text-gray-500 dark:text-slate-400">|</span>
<span class="text-xs text-gray-500 dark:text-slate-400" data-l10n="Polling">Polling</span>
<button id="pli-pause-btn" onclick="pysimPollToggle()" class="px-2.5 py-1 text-sm rounded bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600">OFF</button>
<span id="pli-poll-stat" class="text-xs text-gray-400"></span>
</div>
<div class="mt-2 mb-2 text-sm text-gray-500 dark:text-slate-400" data-l10n="Events that the card monitors (SET UP EVENT LIST):">Events that the card monitors (SET UP EVENT LIST):</div>
<div id="pysim-event-list" class="text-sm font-mono text-gray-600 dark:text-slate-400"></div>
<div class="mt-4 mb-2 text-sm text-gray-500 dark:text-slate-400" data-l10n="Fetched proactive commands:">Fetched proactive commands:</div>
<div id="pysim-proactive-log" class="text-sm font-mono text-gray-600 dark:text-slate-400"></div>
</div>
<div>
<div class="flex justify-between items-center mb-2">
<span class="text-sm text-gray-500 dark:text-slate-400" data-l10n="PROVIDE LOCAL INFORMATION response data">PROVIDE LOCAL INFORMATION response data</span>
<button id="pli-save-btn" onclick="pysimPliSave()" class="px-2.5 py-1 text-sm rounded bg-emerald-600 text-white hover:bg-emerald-700" data-l10n="Save">Save</button>
</div>
<div id="pysim-pli-dict" class="text-sm font-mono text-gray-600 dark:text-slate-400 max-h-[60vh] overflow-auto">
<span class="text-gray-400" data-l10n="Loading...">Loading...</span>
</div>
</div>
</div>
</div>
<div id="event-send-modal" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center hidden">
<div class="bg-white dark:bg-slate-800 rounded-lg p-6 max-w-sm w-full mx-4 max-h-[85vh] overflow-auto">
<h3 id="event-send-title" class="text-lg font-semibold mb-4 text-gray-800 dark:text-slate-200"></h3>
<div id="event-send-body" class="space-y-3"></div>
<div id="event-send-error" class="text-xs text-red-500 hidden mt-2"></div>
<div id="event-send-result" class="text-xs text-emerald-600 dark:text-emerald-400 hidden mt-2"></div>
<div class="flex gap-2 mt-4 justify-end">
<button onclick="document.getElementById('event-send-modal').classList.add('hidden')" class="px-3 py-1.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-100 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300" data-l10n="Cancel">Cancel</button>
<button id="event-send-btn" onclick="pysimEventSend()" class="px-4 py-1.5 text-xs rounded bg-blue-600 text-white hover:bg-blue-700" data-l10n="Send">Send</button>
</div>
</div>
</div>
</div>
<div id="stk-menu-panel" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center hidden">
<div class="bg-white dark:bg-slate-800 rounded-lg p-4 max-w-xs w-full max-h-[80vh] flex flex-col">
<div class="flex justify-between items-center mb-3">
<button id="stk-back-btn" onclick="stkMenuRespond('back')" class="px-3 py-1 text-xs rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" style="display:none" data-l10n="← Back">← Back</button>
<span id="stk-menu-panel-title" class="font-bold text-sm flex-1 text-center"></span>
<button onclick="stkMenuExit()" class="px-3 py-1 text-xs rounded-full bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400 hover:bg-red-200 dark:hover:bg-red-900/50" data-l10n="Exit">Exit</button>
</div>
<div id="stk-menu-panel-content" class="flex-1 overflow-auto"></div>
<div id="stk-menu-buttons" class="flex justify-between mt-3 pt-3 border-t border-gray-200 dark:border-slate-700 hidden">
<button id="stk-timeout-btn" onclick="stkMenuRespond('timeout')" class="px-3 py-1 text-xs rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-l10n="Timeout">Timeout</button>
<button id="stk-cancel-btn" onclick="stkMenuRespond('cancel')" class="px-3 py-1 text-xs rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-l10n="Cancel">Cancel</button>
<button id="stk-ok-btn" onclick="stkMenuRespond('ok')" class="px-4 py-1 text-xs rounded-full bg-blue-600 text-white hover:bg-blue-700" data-l10n="OK">OK</button>
</div>
</div>
</div>
</div>
<script>
// ===== Tab switching =====
function switchTab(name) {
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
document.getElementById('tab-' + name).classList.remove('hidden');
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('bg-blue-600', 'text-white', 'dark:bg-blue-500', 'dark:text-white');
btn.classList.add('bg-gray-200', 'text-gray-700', 'dark:bg-slate-700', 'dark:text-slate-300');
});
const active = document.querySelector(`.tab-btn[data-tab="${name}"]`);
active.classList.remove('bg-gray-200', 'text-gray-700', 'dark:bg-slate-700', 'dark:text-slate-300');
active.classList.add('bg-blue-600', 'text-white', 'dark:bg-blue-500', 'dark:text-white');
if (name === 'c-apdu') cApduSwitchSubtab('sim');
else if (name === 'scp80') setHelpAnchor(scp80HelpAnchor);
else if (name === 'response') setHelpAnchor('response-parser');
else if (name === 'pysim') setHelpAnchor(pysimHelpAnchor);
}
let scp80HelpAnchor = 'secured-packet';
function scp80SwitchSubtab(name) {
document.querySelectorAll('.scp80-subtab').forEach(btn => {
const isActive = btn.dataset.scp80Sub === name;
btn.classList.toggle('bg-blue-600', isActive);
btn.classList.toggle('text-white', isActive);
btn.classList.toggle('bg-gray-200', !isActive);
btn.classList.toggle('dark:bg-slate-700', !isActive);
btn.classList.toggle('text-gray-700', !isActive);
btn.classList.toggle('dark:text-slate-300', !isActive);
});
document.getElementById('scp80-sub-sp').classList.toggle('hidden', name !== 'sp');
document.getElementById('scp80-sub-cards').classList.toggle('hidden', name !== 'cards');
scp80HelpAnchor = { sp: 'secured-packet', cards: 'cards' }[name] || 'secured-packet';
setHelpAnchor(scp80HelpAnchor);
}
function cApduSwitchSubtab(name) {
document.querySelectorAll('.c-apdu-subtab').forEach(btn => {
const active = btn.dataset.sub === name;
btn.classList.toggle('bg-blue-600', active);
btn.classList.toggle('text-white', active);
btn.classList.toggle('bg-gray-200', !active);
btn.classList.toggle('dark:bg-slate-700', !active);
btn.classList.toggle('text-gray-700', !active);
btn.classList.toggle('dark:text-slate-300', !active);
});
['sim', 'usim', 'ber', 'ram', 'parse'].forEach(s => {
const el = document.getElementById('c-apdu-sub-' + s);
if (el) el.classList.toggle('hidden', s !== name);
});
if (name === 'sim' || name === 'usim') {
updateSimUsimFields(name);
updateSimSelectMethod(name);
}
setHelpAnchor({sim:'sim-rfm', usim:'usim-rfm', ber:'ber-tlv', ram:'ram-gp', parse:'c-apdu'}[name] || 'c-apdu');
}
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => switchTab(btn.dataset.tab));
});
// ===== SIM/USIM field visibility =====
const RECORD_OPS = new Set(['update-record', 'read-record']);
const BINARY_OPS = new Set(['update-binary', 'read-binary', 'erase-binary']);
const READ_OPS = new Set(['read-record', 'read-binary']);
const DATA_OPS = new Set(['update-record', 'update-binary']);
const RECSIZE_OPS = new Set(['update-record']);
const PIN_OPS = new Set(['verify', 'change', 'disable', 'enable', 'unblock']);
function updateSimUsimFields(mode) {
if (!mode) return;
const cmd = document.getElementById(mode + '-cmd').value;
document.getElementById(mode + '-record-row').style.display = RECORD_OPS.has(cmd) ? '' : 'none';
document.getElementById(mode + '-offset-row').style.display = BINARY_OPS.has(cmd) ? '' : 'none';
document.getElementById(mode + '-le-row').style.display = READ_OPS.has(cmd) ? '' : 'none';
document.getElementById(mode + '-data-row').style.display = DATA_OPS.has(cmd) ? '' : 'none';
document.getElementById(mode + '-recsize-row').style.display = RECSIZE_OPS.has(cmd) ? '' : 'none';
document.getElementById(mode + '-pin-row').style.display = PIN_OPS.has(cmd) ? '' : 'none';
const pinValCmds = cmd === 'verify' || cmd === 'disable' || cmd === 'enable';
document.getElementById(mode + '-pin-val-row').style.display = pinValCmds ? '' : 'none';
const oldNewCmds = cmd === 'change' || cmd === 'unblock';
document.getElementById(mode + '-pin-old-row').style.display = oldNewCmds ? '' : 'none';
document.getElementById(mode + '-pin-new-row').style.display = oldNewCmds ? '' : 'none';
if (oldNewCmds) {
document.querySelector('#' + mode + '-pin-old-row label').textContent = cmd === 'unblock' ? 'Unblock PIN' : 'Old PIN';
}
document.getElementById(mode + '-act-row').style.display = (cmd === 'activate-file' || cmd === 'deactivate-file') ? '' : 'none';
if (cmd === 'select') {
document.getElementById(mode + '-select-checkbox-row').style.display = 'none';
} else {
document.getElementById(mode + '-select-checkbox-row').style.display = '';
}
updateP1P2Display(mode);
}
function updateSimSelectMethod(mode) {
if (!mode) return;
const method = document.getElementById(mode + '-sel-method').value;
document.getElementById(mode + '-sel-fid-row').style.display = method === 'fid' ? '' : 'none';
document.getElementById(mode + '-sel-path-row').style.display = method === 'path' ? '' : 'none';
document.getElementById(mode + '-sel-dfname-row').style.display = method === 'dfname' ? '' : 'none';
document.getElementById(mode + '-sel-chain-row').style.display = method === 'chain' ? '' : 'none';
}
function updateActFileInput(mode) {
const target = document.getElementById(mode + '-act-target').value;
document.getElementById(mode + '-act-file').style.display = target === 'current' ? 'none' : '';
}
function toggleSimOverride(mode) {
const checked = document.getElementById(mode + '-override').checked;
document.getElementById(mode + '-p1').disabled = !checked;
document.getElementById(mode + '-p2').disabled = !checked;
}
function updateSimSelectSection(mode) {
const cmd = document.getElementById(mode + '-cmd').value;
if (cmd === 'select') return;
document.getElementById(mode + '-select-method-body').style.display =
document.getElementById(mode + '-select').checked ? '' : 'none';
}
function updateP1P2Display(mode) {
const preset = mode === 'sim' ? {selP1: 0x00, selP2: 0x00} : {selP1: 0x00, selP2: 0x04};
const cmd = document.getElementById(mode + '-cmd').value;
if (cmd === 'select') {
const method = document.getElementById(mode + '-sel-method').value;
const silent = mode === 'usim' && !!document.getElementById('usim-sel-silent').checked;
const p2Base = silent ? 0x0C : preset.selP2;
let p1, p2;
if (method === 'fid' || method === 'chain') {
p1 = preset.selP1; p2 = p2Base;
} else if (method === 'path') {
p1 = (mode === 'usim' && document.querySelector('input[name="usim-sel-base"]:checked')?.value === 'df') ? 0x09 : 0x08;
p2 = mode === 'sim' ? 0x00 : p2Base;
} else if (method === 'dfname') {
p1 = 0x04; p2 = mode === 'sim' ? 0x00 : p2Base;
} else {
p1 = preset.selP1; p2 = p2Base;
}
document.getElementById(mode + '-p1').value = p1.toString(16).padStart(2, '0').toUpperCase();
document.getElementById(mode + '-p2').value = p2.toString(16).padStart(2, '0').toUpperCase();
return;
}
const record = parseInt(document.getElementById(mode + '-record').value) || 1;
const recMode = parseInt(document.getElementById(mode + '-rec-mode').value, 16);
const offsetHex = (document.getElementById(mode + '-offset').value || '0000').replace(/[^0-9a-fA-F]/g, '').padStart(4, '0').slice(0, 4);
const offset = parseInt(offsetHex, 16);
const pin = document.getElementById(mode + '-pin').value;
let p1, p2;
if (cmd === 'read-record' || cmd === 'update-record') {
p1 = (recMode === 0x02 || recMode === 0x03) ? 0x00 : record; p2 = recMode;
} else if (cmd === 'read-binary' || cmd === 'update-binary' || cmd === 'erase-binary') {
p1 = (offset >> 8) & 0xFF; p2 = offset & 0xFF;
} else if (cmd === 'activate-file' || cmd === 'deactivate-file') {
const target = document.getElementById(mode + '-act-target').value;
if (target === 'current') {
p1 = 0x00; p2 = 0x00;
} else {
p1 = target === 'fid' ? 0x00 : (target === 'mfpath' ? 0x08 : 0x09);
p2 = 0x00;
}
} else if (cmd === 'verify' || cmd === 'change' || cmd === 'disable' || cmd === 'enable' || cmd === 'unblock') {
p1 = 0x00; p2 = parseInt(pin, 16);
} else {
p1 = 0x00; p2 = 0x00;
}
document.getElementById(mode + '-p1').value = p1.toString(16).padStart(2, '0').toUpperCase();
document.getElementById(mode + '-p2').value = p2.toString(16).padStart(2, '0').toUpperCase();
}
// ===== RAM field visibility =====
function updateRamFields() {
const cmd = document.getElementById('ram-cmd').value;
const showPriv = ['install-install', 'install-make-sel', 'install-reg-update'].includes(cmd);
document.getElementById('ram-priv-row').style.display = showPriv ? '' : 'none';
document.getElementById('ram-toolkit-row').style.display = cmd === 'install-install' ? '' : 'none';
document.getElementById('ram-sdaid-row').style.display = ['install-load', 'install-reg-update', 'install-extradition'].includes(cmd) ? '' : 'none';
document.getElementById('ram-elfaid-row').style.display = cmd === 'install-install' ? '' : 'none';
document.getElementById('ram-modaid-row').style.display = cmd === 'install-install' ? '' : 'none';
document.getElementById('ram-load-data-row').style.display = cmd === 'install-load' ? '' : 'none';
document.getElementById('ram-data-row').style.display = ['load', 'store-data', 'ext-auth', 'int-auth'].includes(cmd) ? '' : 'none';
document.getElementById('ram-block-row').style.display = ['load', 'store-data'].includes(cmd) ? '' : 'none';
document.getElementById('ram-enc-row').style.display = cmd === 'store-data' ? '' : 'none';
document.getElementById('ram-authp1-row').style.display = cmd === 'ext-auth' ? '' : 'none';
document.getElementById('ram-del-mode-row').style.display = cmd === 'delete' ? '' : 'none';
document.getElementById('ram-gs-mode-row').style.display = cmd === 'get-status' ? '' : 'none';
document.getElementById('ram-gs-p2-row').style.display = cmd === 'get-status' ? '' : 'none';
document.getElementById('ram-tag-row').style.display = cmd === 'get-data' ? '' : 'none';
document.getElementById('ram-ss-mode-row').style.display = cmd === 'set-status' ? '' : 'none';
document.getElementById('ram-ss-state-row').style.display = cmd === 'set-status' ? '' : 'none';
updateRamToolkit();
}
function updateRamPriv() {
let b1 = 0, b2 = 0, b3 = 0;
document.querySelectorAll('.ram-priv-b1:checked').forEach(cb => b1 |= parseInt(cb.value, 16));
document.querySelectorAll('.ram-priv-b2:checked').forEach(cb => b2 |= parseInt(cb.value, 16));
document.querySelectorAll('.ram-priv-b3:checked').forEach(cb => b3 |= parseInt(cb.value, 16));
let hex = b1.toString(16).padStart(2, '0').toUpperCase();
hex += b2.toString(16).padStart(2, '0').toUpperCase();
if (b3) hex += b3.toString(16).padStart(2, '0').toUpperCase();
document.getElementById('ram-priv').value = hex;
}
function updateRamToolkit() {
const checked = document.getElementById('ram-toolkit-enable').checked;
document.getElementById('ram-toolkit-body').style.display = checked ? '' : 'none';
updateRamTkMode();
}
function updateRamTkMode() {
const isSim = document.getElementById('ram-tk-mode').value === 'ca';
document.getElementById('ram-tk-ad-row').style.display = isSim ? '' : 'none';
document.getElementById('ram-tk-services-row').style.display = isSim ? 'none' : '';
}
document.getElementById('ram-tk-mode').addEventListener('change', updateRamTkMode);
// ===== GSM 7-bit alphabet =====
const GSM7_ALPHABET = [
'\u0040','\u00a3','\u0024','\u00a5','\u00e8','\u00e9','\u00f9','\u00ec',
'\u00f2','\u00c7','\u000a','\u00d8','\u00f8','\u000d','\u00c5','\u00e5',
'\u0394','\u005f','\u03a6','\u0393','\u039b','\u03a9','\u03a0','\u03a8',
'\u03a3','\u0398','\u039e','\u001b','\u00c6','\u00e6','\u00df','\u00c9',
'\u0020','\u0021','\u0022','\u0023','\u00a4','\u0025','\u0026','\u0027',
'\u0028','\u0029','\u002a','\u002b','\u002c','\u002d','\u002e','\u002f',
'\u0030','\u0031','\u0032','\u0033','\u0034','\u0035','\u0036','\u0037',
'\u0038','\u0039','\u003a','\u003b','\u003c','\u003d','\u003e','\u003f',
'\u00a1','\u0041','\u0042','\u0043','\u0044','\u0045','\u0046','\u0047',
'\u0048','\u0049','\u004a','\u004b','\u004c','\u004d','\u004e','\u004f',
'\u0050','\u0051','\u0052','\u0053','\u0054','\u0055','\u0056','\u0057',
'\u0058','\u0059','\u005a','\u00c4','\u00d6','\u00d1','\u00dc','\u00a7',
'\u00bf','\u0061','\u0062','\u0063','\u0064','\u0065','\u0066','\u0067',
'\u0068','\u0069','\u006a','\u006b','\u006c','\u006d','\u006e','\u006f',
'\u0070','\u0071','\u0072','\u0073','\u0074','\u0075','\u0076','\u0077',
'\u0078','\u0079','\u007a','\u00e4','\u00f6','\u00f1','\u00fc','\u00e0',
];
const GSM7_EXT_MAP = {
'\n': 0x0a, '^': 0x14, '{': 0x28, '}': 0x29, '\\': 0x2f,
'[': 0x3c, '~': 0x3d, ']': 0x3e, '|': 0x40, '\u20ac': 0x65,
};
function gsm7TextToSeptets(text) {
const septets = [];
for (const c of text) {
if (c in GSM7_EXT_MAP) {
septets.push(0x1B);
septets.push(GSM7_EXT_MAP[c]);
} else {
const idx = GSM7_ALPHABET.indexOf(c);
if (idx === -1) throw new Error('Character not in GSM7 alphabet');
septets.push(idx);
}
}
return septets;
}
function gsm7Encode(septets) {
const numOctets = Math.ceil(septets.length * 7 / 8);
const packed = new Uint8Array(numOctets);
for (let i = 0; i < septets.length; i++) {
const bitPos = i * 7;
const byteIdx = bitPos >>> 3;
const bitShift = bitPos & 7;
packed[byteIdx] |= (septets[i] << bitShift) & 0xFF;
if (byteIdx + 1 < numOctets) {
packed[byteIdx + 1] |= septets[i] >>> (8 - bitShift);
}
}
return packed;
}
function encodeSpn(text) {
try {
const septets = gsm7TextToSeptets(text);
const packed = gsm7Encode(septets);
const nSeptets = septets.length;
const nOctets = packed.length;
const lastOctetDataBits = nSeptets * 7 - (nOctets - 1) * 8;
const spareBits = 8 - lastOctetDataBits;
let dcs = 0x80 | (spareBits & 0x07);
if (dcs === 0x81) dcs = 0x80;
const inner = new Uint8Array(16);
inner[0] = dcs;
inner.set(packed, 1);
for (let i = 1 + nOctets; i < 16; i++) inner[i] = 0xFF;
return '01' + Array.from(inner).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join('');
} catch (e) {
let hasNonBmp = false;
for (const c of text) {
const code = c.charCodeAt(0);
if (code >= 0xD800 && code <= 0xDBFF) { hasNonBmp = true; break; }
}
if (hasNonBmp) {
const buf = [];
for (const c of text) {
const code = c.charCodeAt(0);
buf.push(code >>> 8, code & 0xFF);
}
const inner = new Uint8Array(16);
inner[0] = 0x80;
for (let i = 0; i < buf.length && i + 1 < 16; i++) inner[i + 1] = buf[i];
for (let i = 1 + buf.length; i < 16; i++) inner[i] = 0xFF;
return '00' + Array.from(inner).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join('');
} else {
const nonGsm7 = [];
for (const c of text) {
if (GSM7_ALPHABET.indexOf(c) === -1 && !(c in GSM7_EXT_MAP)) {
nonGsm7.push(c.charCodeAt(0));
}
}
const base = nonGsm7.length ? (Math.min(...nonGsm7) & 0xFF00) : 0;
const baseHigh = base >>> 8;
const inner = new Uint8Array(16);
inner[0] = 0x81;
inner[1] = text.length;
inner[2] = baseHigh;
let pos = 3;
for (const c of text) {
if (GSM7_EXT_MAP.hasOwnProperty(c)) {
inner[pos++] = GSM7_EXT_MAP[c];
} else {
const idx = GSM7_ALPHABET.indexOf(c);
if (idx !== -1) {
inner[pos++] = idx;
} else {
const code = c.charCodeAt(0);
inner[pos++] = 0x80 | (code - base);
}
}
}
while (pos < 16) inner[pos++] = 0xFF;
return '00' + Array.from(inner).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join('');
}
}
}
function swapNibbles(s) {
let out = '';
for (let i = 0; i < s.length; i += 2) {
out += s[i + 1] || 'f';
out += s[i];
}
return out.toUpperCase();
}
function encImsi(imsi) {
const l = Math.ceil((imsi.length + 1) / 2);
const oe = imsi.length & 1;
const indicator = ((oe << 3) | 1).toString(16);
const padded = imsi.padEnd(15, 'f');
const second = swapNibbles(indicator + padded);
return l.toString(16).padStart(2, '0') + second;
}
function encMsisdn(msisdn) {
let s = msisdn.replace(/^\+/, '');
if (s.length % 2) s += 'f';
return swapNibbles(s);
}
function encIccid(iccid) {
return swapNibbles(iccid);
}
function buildApdu(cla, ins, p1, p2, p3, data) {
let s = cla.toString(16).padStart(2, '0').toUpperCase() +
ins.toString(16).padStart(2, '0').toUpperCase() +
p1.toString(16).padStart(2, '0').toUpperCase() +
p2.toString(16).padStart(2, '0').toUpperCase();
if (p3 !== null && p3 !== undefined) s += p3.toString(16).padStart(2, '0').toUpperCase();
if (data) s += data.toUpperCase();
return s;
}
function buildSelect(cla, p1, p2, fid) {
const data = fid.length === 4 ? fid : '0000';
return buildApdu(cla, 0xA4, p1, p2, 0x02, data);
}
const OPS = {
'select': {ins: 0xA4},
'read-record': {ins: 0xB2},
'read-binary': {ins: 0xB0},
'update-record': {ins: 0xDC},
'update-binary': {ins: 0xD6},
'erase-binary': {ins: 0x0E},
'activate-file': {ins: 0x44},
'deactivate-file': {ins: 0x04},
'verify': {ins: 0x20},
'change': {ins: 0x24},
'disable': {ins: 0x26},
'enable': {ins: 0x28},
'unblock': {ins: 0x2C},
};
function getSelValue(mode) {
const method = document.getElementById(mode + '-sel-method').value;
if (method === 'fid') {
return {type: 'fid', value: (document.getElementById(mode + '-sel-fid').value || '').replace(/[^0-9a-fA-F]/g, '')};
} else if (method === 'path') {
return {type: 'path', value: (document.getElementById(mode + '-sel-path').value || '').replace(/[^0-9a-fA-F]/g, '')};
} else if (method === 'dfname') {
return {type: 'dfname', value: (document.getElementById(mode + '-sel-dfname').value || '').replace(/[^0-9a-fA-F]/g, '')};
} else if (method === 'chain') {
return {type: 'chain', value: (document.getElementById(mode + '-sel-chain').value || '').trim()};
}
return null;
}
function buildSelectApdu(mode, preset, selInfo) {
const isSim = preset.cla === 0xA0;
if (!selInfo || !selInfo.value) return '';
// USIM response control: FCP request (P2=04 + Le) vs silent hop (P2=0C, no Le)
const silent = !isSim && !!document.getElementById('usim-sel-silent').checked;
const p2 = silent ? 0x0C : preset.selP2;
const le = (!isSim && !silent) ? '00' : null;
if (selInfo.type === 'fid') {
if (selInfo.value.length !== 4) return '';
let apdu = buildSelect(preset.cla, preset.selP1, p2, selInfo.value);
if (le) apdu += le;
return apdu;
} else if (selInfo.type === 'path') {
if (selInfo.value.length < 2 || selInfo.value.length % 2 !== 0) return '';
const p3 = selInfo.value.length / 2;
let p1 = isSim ? 0x08 : (document.querySelector('input[name="usim-sel-base"]:checked')?.value === 'df' ? 0x09 : 0x08);
let apdu = buildApdu(preset.cla, 0xA4, p1, p2, p3, selInfo.value);
if (le) apdu += le;
return apdu;
} else if (selInfo.type === 'dfname') {
if (selInfo.value.length < 2) return '';
const p3 = selInfo.value.length / 2;
let apdu = buildApdu(preset.cla, 0xA4, 0x04, p2, p3, selInfo.value);
if (le) apdu += le;
return apdu;
} else if (selInfo.type === 'chain') {
const tokens = selInfo.value.split(',').map(t => t.trim()).filter(t => t.length > 0);
let out = '';
for (const tok of tokens) {
// 'C0' or 'C0:NN' - GET RESPONSE hop (Le defaults to '00')
const gr = /^c0(?::([0-9a-fA-F]{1,2}))?$/i.exec(tok);
if (gr) {
const leByte = parseInt(gr[1] || '00', 16);
out += buildApdu(preset.cla, 0xC0, 0x00, 0x00, leByte, '');
continue;
}
const fid = tok.replace(/[^0-9a-fA-F]/g, '');
if (fid.length !== 4) continue;
let apdu = buildSelect(preset.cla, preset.selP1, p2, fid);
if (le) apdu += le;
out += apdu;
}
return out;
}
return '';
}
function genSimUsim(mode) {
const preset = mode === 'sim' ? {cla: 0xA0, selP1: 0x00, selP2: 0x00} : {cla: 0x00, selP1: 0x00, selP2: 0x04};
const cmd = document.getElementById(mode + '-cmd').value;
const doSelect = document.getElementById(mode + '-select').checked;
const selInfo = getSelValue(mode);
const record = parseInt(document.getElementById(mode + '-record').value) || 1;
const recModeHex = document.getElementById(mode + '-rec-mode').value;
const recMode = parseInt(recModeHex, 16);
const offsetHex = (document.getElementById(mode + '-offset').value || '0000').replace(/[^0-9a-fA-F]/g, '').padStart(4, '0').slice(0, 4);
const offset = parseInt(offsetHex, 16);
const leHex = (document.getElementById(mode + '-le').value || '00').replace(/[^0-9a-fA-F]/g, '').padStart(2, '0').slice(0, 2);
const le = parseInt(leHex, 16);
let dataHex = (document.getElementById(mode + '-data').value || '').replace(/[^0-9a-fA-F]/g, '');
const recSize = parseInt(document.getElementById(mode + '-recsize').value) || 0;
const pin = document.getElementById(mode + '-pin').value;
const override = document.getElementById(mode + '-override').checked;
const overrideP1 = override ? parseInt((document.getElementById(mode + '-p1').value || '00').replace(/[^0-9a-fA-F]/g, '').padStart(2, '0').slice(0, 2), 16) : null;
const overrideP2 = override ? parseInt((document.getElementById(mode + '-p2').value || '00').replace(/[^0-9a-fA-F]/g, '').padStart(2, '0').slice(0, 2), 16) : null;
const resultEl = document.getElementById(mode + '-result');
if (recSize > 0 && dataHex.length > 0) {
const targetBytes = recSize * 2;
if (dataHex.length > targetBytes) dataHex = dataHex.slice(0, targetBytes);
else dataHex = dataHex.padEnd(targetBytes, 'F');
}
if (cmd === 'select') {
const selectApdu = buildSelectApdu(mode, preset, selInfo);
if (!selectApdu) { resultEl.value = 'Error: specify SELECT parameters'; return; }
resultEl.value = selectApdu;
document.getElementById(mode + '-pack-btn').disabled = false;
return;
}
const op = OPS[cmd];
if (!op) { resultEl.value = 'Error: unknown command'; return; }
let p1, p2, p3, data = null;
if (cmd === 'read-record') {
p1 = (recMode === 0x02 || recMode === 0x03) ? 0x00 : record; p2 = recMode; p3 = le;
} else if (cmd === 'read-binary') {
p1 = (offset >> 8) & 0xFF; p2 = offset & 0xFF; p3 = le;
} else if (cmd === 'update-record') {
if (!dataHex) { resultEl.value = 'Error: specify data'; return; }
p1 = (recMode === 0x02 || recMode === 0x03) ? 0x00 : record; p2 = recMode; p3 = dataHex.length / 2; data = dataHex;
} else if (cmd === 'update-binary') {
if (!dataHex) { resultEl.value = 'Error: specify data'; return; }
p1 = (offset >> 8) & 0xFF; p2 = offset & 0xFF; p3 = dataHex.length / 2; data = dataHex;
} else if (cmd === 'erase-binary') {
p1 = (offset >> 8) & 0xFF; p2 = offset & 0xFF; p3 = 0x00;
} else if (cmd === 'activate-file' || cmd === 'deactivate-file') {
const target = document.getElementById(mode + '-act-target').value;
if (target === 'current') {
p1 = 0x00; p2 = 0x00; p3 = null;
} else {
const fileHex = (document.getElementById(mode + '-act-file').value || '').replace(/[^0-9a-fA-F]/g, '');
if (!fileHex || fileHex.length % 2 !== 0) { resultEl.value = 'Error: specify FID or path (even hex length)'; return; }
p1 = target === 'fid' ? 0x00 : (target === 'mfpath' ? 0x08 : 0x09);
p2 = 0x00;
p3 = fileHex.length / 2;
data = fileHex;
}
} else if (cmd === 'verify' || cmd === 'disable' || cmd === 'enable') {
const pinVal = document.getElementById(mode + '-pin-val').value || '';
if (!pinVal) { resultEl.value = 'Error: specify PIN value'; return; }
const pinHex = Array.from(pinVal).map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join('').padEnd(16, 'F').slice(0, 16);
p1 = 0x00; p2 = parseInt(pin, 16); p3 = 0x08; data = pinHex;
} else if (cmd === 'change' || cmd === 'unblock') {
const oldPin = document.getElementById(mode + '-pin-old').value || '';
const newPin = document.getElementById(mode + '-pin-new').value || '';
if (!oldPin || !newPin) { resultEl.value = 'Error: specify old and new PIN'; return; }
const oldHex = Array.from(oldPin).map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join('').padEnd(16, 'F').slice(0, 16);
const newHex = Array.from(newPin).map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join('').padEnd(16, 'F').slice(0, 16);
p1 = 0x00; p2 = parseInt(pin, 16); p3 = 0x10; data = oldHex + newHex;
}
if (override) {
if (overrideP1 !== null) p1 = overrideP1;
if (overrideP2 !== null) p2 = overrideP2;
}
let apdu = '';
if (doSelect) {
const selectApdu = buildSelectApdu(mode, preset, selInfo);
if (selectApdu) {
// full CLA on every command - scripted secured-packet streams must be
// self-delimiting; implied-CLA fragments desync the card's parser
apdu = selectApdu + buildApdu(preset.cla, op.ins, p1, p2, p3, data);
} else {
apdu = buildApdu(preset.cla, op.ins, p1, p2, p3, data);
}
} else {
apdu = buildApdu(preset.cla, op.ins, p1, p2, p3, data);
}
resultEl.value = apdu;
document.getElementById(mode + '-pack-btn').disabled = false;
}
function genRam() {
const cmd = document.getElementById('ram-cmd').value;
const aid = (document.getElementById('ram-aid').value || '').replace(/[^0-9a-fA-F]/g, '');
const sdAid = (document.getElementById('ram-sdaid').value || '').replace(/[^0-9a-fA-F]/g, '');
const elfAid = (document.getElementById('ram-elfaid').value || '').replace(/[^0-9a-fA-F]/g, '');
const modAid = (document.getElementById('ram-modaid').value || '').replace(/[^0-9a-fA-F]/g, '');
const loadData = (document.getElementById('ram-load-data').value || '').replace(/[^0-9a-fA-F]/g, '');
const ramData = (document.getElementById('ram-data').value || '').replace(/[^0-9a-fA-F]/g, '');
const blockNum = parseInt(document.getElementById('ram-block').value) || 0;
const enc = document.getElementById('ram-enc').value;
const delMode = document.getElementById('ram-del-mode').value;
const gsMode = document.getElementById('ram-gs-mode').value;
const tagHex = (document.getElementById('ram-tag').value || '5F50').replace(/[^0-9a-fA-F]/g, '').padStart(4, '0').slice(0, 4);
const ssMode = parseInt(document.getElementById('ram-ss-mode').value, 16);
const ssState = parseInt(document.getElementById('ram-ss-state').value, 16);
const resultEl = document.getElementById('ram-result');
const INSTALL_P1 = {
'install-load': 0x02,
'install-install': 0x0C,
'install-make-sel': 0x08,
'install-reg-update': 0x40,
'install-extradition': 0x10,
};
function lv(data) {
if (!data) return '00';
return berLenStr(data.length / 2) + data;
}
function computePriv() {
const privHex = document.getElementById('ram-priv').value;
const b1 = parseInt(privHex.substr(0, 2), 16) || 0;
const b2 = privHex.length >= 4 ? parseInt(privHex.substr(2, 2), 16) : 0;
const b3 = privHex.length >= 6 ? parseInt(privHex.substr(4, 2), 16) : 0;
if (b2 === 0 && b3 === 0) return b1.toString(16).padStart(2, '0').toUpperCase();
return b1.toString(16).padStart(2, '0').toUpperCase() + b2.toString(16).padStart(2, '0').toUpperCase() + b3.toString(16).padStart(2, '0').toUpperCase();
}
function buildToolkitParams() {
if (!document.getElementById('ram-toolkit-enable').checked) return '';
const tkMode = document.getElementById('ram-tk-mode').value;
const priority = parseInt(document.getElementById('ram-tk-priority').value) || 0;
const timers = parseInt(document.getElementById('ram-tk-timers').value) || 0;
const textLen = parseInt(document.getElementById('ram-tk-textlen').value) || 0;
const menus = parseInt(document.getElementById('ram-tk-menus').value) || 0;
const firstPos = parseInt(document.getElementById('ram-tk-firstpos').value) || 0;
const firstId = (document.getElementById('ram-tk-firstid').value || '00').replace(/[^0-9a-fA-F]/g, '').padStart(2, '0').slice(0, 2) || '00';
const lastPos = parseInt(document.getElementById('ram-tk-lastpos').value) || 0;
const lastId = (document.getElementById('ram-tk-lastid').value || '00').replace(/[^0-9a-fA-F]/g, '').padStart(2, '0').slice(0, 2) || '00';
const channels = parseInt(document.getElementById('ram-tk-channels').value) || 0;
const msl = document.getElementById('ram-tk-msl').value;
const tar = (document.getElementById('ram-tk-tar').value || '').replace(/[^0-9a-fA-F]/g, '');
const adRaw = (document.getElementById('ram-tk-ad').value || '').replace(/[^0-9a-fA-F]/g, '');
const ad = adRaw.padStart(2, '0').slice(0, 2);
const services = parseInt(document.getElementById('ram-tk-services').value) || 0;
if (tar && tar.length % 6 !== 0) { resultEl.value = 'Error: TAR must be 3-byte multiples'; return null; }
if (parseInt(firstId, 16) > 0x7F || parseInt(lastId, 16) > 0x7F) { resultEl.value = 'Error: menu IDs must be <= 7F'; return null; }
let menuPairs = '';
for (let i = 1; i <= menus; i++) {
if (i === 1) menuPairs += firstPos.toString(16).padStart(2, '0') + firstId;
else if (i === menus) menuPairs += lastPos.toString(16).padStart(2, '0') + lastId;
else menuPairs += '0000';
}
const mslField = msl === '00' ? '00' : '0201' + msl;
let tkPayload = '';
if (tkMode === 'ca') {
tkPayload = (adRaw ? '01' + ad : '00') +
priority.toString(16).padStart(2, '0') +
timers.toString(16).padStart(2, '0') +
textLen.toString(16).padStart(2, '0') +
menus.toString(16).padStart(2, '0') +
menuPairs +
channels.toString(16).padStart(2, '0') +
mslField +
(tar ? (tar.length / 2).toString(16).padStart(2, '0') + tar : '00');
const caTlv = 'CA' + berLenStr(tkPayload.length / 2) + tkPayload;
return 'EF' + berLenStr(caTlv.length / 2) + caTlv;
} else {
tkPayload = priority.toString(16).padStart(2, '0') +
timers.toString(16).padStart(2, '0') +
textLen.toString(16).padStart(2, '0') +
menus.toString(16).padStart(2, '0') +
menuPairs +
channels.toString(16).padStart(2, '0') +
mslField +
(tar ? (tar.length / 2).toString(16).padStart(2, '0') + tar : '00') +
services.toString(16).padStart(2, '0');
const innerTlv = '80' + berLenStr(tkPayload.length / 2) + tkPayload;
return 'EA' + berLenStr(innerTlv.length / 2) + innerTlv;
}
}
let apdu = '';
if (cmd === 'install-load') {
if (!aid) { resultEl.value = 'Error: specify Load File AID'; return; }
let dataField = lv(aid) + lv(sdAid) + '00' + (loadData ? lv(loadData) : '00') + '00';
const dataLen = berLenStr(dataField.length / 2);
apdu = buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dataLen, 16), dataField) + '00';
} else if (cmd === 'install-install') {
if (!aid) { resultEl.value = 'Error: specify AID'; return; }
const privBytes = computePriv();
let installParams = 'C900';
const tkParams = buildToolkitParams();
if (tkParams === null) return;
if (tkParams) installParams += tkParams;
let dataField = lv(elfAid) + lv(modAid) + lv(aid) + lv(privBytes) + lv(installParams) + '00';
const dataLen = berLenStr(dataField.length / 2);
apdu = buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dataLen, 16), dataField) + '00';
} else if (cmd === 'install-make-sel') {
if (!aid) { resultEl.value = 'Error: specify AID'; return; }
const privBytes = computePriv();
let dataField = '00' + '00' + lv(aid) + lv(privBytes) + '00' + '00';
const dataLen = berLenStr(dataField.length / 2);
apdu = buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dataLen, 16), dataField) + '00';
} else if (cmd === 'install-reg-update') {
if (!aid) { resultEl.value = 'Error: specify AID'; return; }
const privBytes = computePriv();
let dataField = lv(sdAid) + '00' + lv(aid) + lv(privBytes) + '00' + '00';
const dataLen = berLenStr(dataField.length / 2);
apdu = buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dataLen, 16), dataField) + '00';
} else if (cmd === 'install-extradition') {
if (!sdAid || sdAid.length < 10 || sdAid.length > 20) { resultEl.value = 'Error: specify SD AID (5-10 bytes)'; return; }
if (!aid) { resultEl.value = 'Error: specify AID'; return; }
let dataField = lv(sdAid) + '00' + lv(aid) + '00' + '00' + '00';
const dataLen = berLenStr(dataField.length / 2);
apdu = buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dataLen, 16), dataField) + '00';
} else if (cmd === 'load') {
if (!ramData) { resultEl.value = 'Error: specify data'; return; }
const dataLen = berLenStr(ramData.length / 2);
apdu = buildApdu(0x80, 0xE8, 0x80, blockNum, parseInt(dataLen, 16), ramData) + '00';
} else if (cmd === 'delete') {
if (!aid) { resultEl.value = 'Error: specify AID'; return; }
const dataField = '4F' + berLenStr(aid.length / 2) + aid;
const dataLen = berLenStr(dataField.length / 2);
apdu = buildApdu(0x80, 0xE4, 0x00, parseInt(delMode, 16), parseInt(dataLen, 16), dataField) + '00';
} else if (cmd === 'get-status') {
const gsP2 = document.getElementById('ram-gs-p2').value;
apdu = buildApdu(0x80, 0xF2, parseInt(gsMode, 16), parseInt(gsP2, 16), 0x02, '4F00') + '00';
} else if (cmd === 'get-data') {
const tag = parseInt(tagHex, 16);
if (tagHex === '2F00' || tagHex === '5031') {
apdu = buildApdu(0x80, 0xCA, (tag >> 8) & 0xFF, tag & 0xFF, 0x02, '5C00') + '00';
} else {
apdu = buildApdu(0x80, 0xCA, (tag >> 8) & 0xFF, tag & 0xFF, 0x00, null) + '00';
}
} else if (cmd === 'store-data') {
if (!ramData) { resultEl.value = 'Error: specify data'; return; }
const dataLen = berLenStr(ramData.length / 2);
apdu = buildApdu(0x80, 0xE2, parseInt(enc, 16), blockNum, parseInt(dataLen, 16), ramData);
} else if (cmd === 'set-status') {
if (ssMode === 0x80) {
apdu = buildApdu(0x80, 0xF0, 0x80, ssState, 0x00, null);
} else {
if (!aid) { resultEl.value = 'Error: specify AID'; return; }
apdu = buildApdu(0x80, 0xF0, ssMode, ssState, aid.length / 2, aid);
}
} else if (cmd === 'ext-auth') {
if (!ramData || ramData.length !== 32) { resultEl.value = 'Error: specify 16-byte data (host cryptogram + MAC)'; return; }
const authP1 = parseInt(document.getElementById('ram-authp1').value, 16);
apdu = buildApdu(0x84, 0x82, authP1, 0x00, 0x10, ramData);
} else if (cmd === 'int-auth') {
if (!ramData) { resultEl.value = 'Error: specify data (challenge)'; return; }
const dataLen = berLenStr(ramData.length / 2);
apdu = buildApdu(0x00, 0x88, 0x00, 0x00, parseInt(dataLen, 16), ramData) + '00';
}
resultEl.value = apdu || 'Error: unknown command';
if (apdu) document.getElementById('ram-pack-btn').disabled = false;
}
function updateConvFields() {
const type = document.getElementById('conv-type').value;
const isPlmn = type === 'plmn' || type === 'plmnwact';
document.getElementById('conv-input').style.display = isPlmn ? 'none' : '';
document.getElementById('conv-plmn-fields').style.display = isPlmn ? '' : 'none';
document.getElementById('conv-act-row').style.display = type === 'plmnwact' ? '' : 'none';
}
function encPlmn(mcc, mnc) {
mcc = mcc.padEnd(3, '0').slice(0, 3);
mnc = mnc.padEnd(3, 'F').slice(0, 3);
const d = mcc + mnc;
const b1 = d[1] + d[0];
const b2 = d[5] + d[2];
const b3 = d[4] + d[3];
return (b1 + b2 + b3).toUpperCase();
}
function genConvert() {
const type = document.getElementById('conv-type').value;
const input = document.getElementById('conv-input').value.trim();
const mcc = document.getElementById('conv-mcc').value.trim();
const mnc = document.getElementById('conv-mnc').value.trim();
const act = document.getElementById('conv-act').value;
let result = '';
try {
if (type === 'imsi') {
if (!input) { result = 'Error: enter a value'; }
else if (!/^\d{15}$/.test(input)) { result = 'Error: IMSI must be 15 digits'; }
else result = encImsi(input);
} else if (type === 'msisdn') {
if (!input) { result = 'Error: enter a value'; }
else result = encMsisdn(input);
} else if (type === 'iccid') {
if (!input) { result = 'Error: enter a value'; }
else result = encIccid(input);
} else if (type === 'spn') {
if (!input) { result = 'Error: enter a value'; }
else result = encodeSpn(input);
} else if (type === 'plmn') {
if (!mcc || !mnc) { result = 'Error: specify MCC and MNC'; }
else if (!/^\d{3}$/.test(mcc)) { result = 'Error: MCC must be 3 digits'; }
else if (!/^\d{2,3}$/.test(mnc)) { result = 'Error: MNC must be 2-3 digits'; }
else result = encPlmn(mcc, mnc);
} else if (type === 'plmnwact') {
if (!mcc || !mnc) { result = 'Error: specify MCC and MNC'; }
else if (!/^\d{3}$/.test(mcc)) { result = 'Error: MCC must be 3 digits'; }
else if (!/^\d{2,3}$/.test(mnc)) { result = 'Error: MNC must be 2-3 digits'; }
else result = encPlmn(mcc, mnc) + act.toUpperCase();
} else if (type === 'nibswap') {
if (!input) { result = 'Error: enter a value'; }
else if (input.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(input)) { result = 'Error: enter an even number of hex characters'; }
else result = swapNibbles(input);
}
} catch (e) {
result = 'Error: ' + e.message;
}
document.getElementById('conv-result').value = result;
}
// ===== Secured Packet UI =====
function packToSp(sourceId) {
const val = document.getElementById(sourceId).value;
if (!val) return;
switchTab('scp80');
scp80SwitchSubtab('sp');
document.getElementById('sp-apdu').value = val;
}
function spCntrAdjust(delta) {
const el = document.getElementById('sp-cntr');
let val = parseInt(el.value.replace(/[^0-9a-fA-F]/g, ''), 16) || 0;
val = Math.max(0, val + delta);
el.value = val.toString(16).padStart(10, '0').toUpperCase();
}
function spInvalidate() {
document.getElementById('sp-result').value = '';
}
function updateSp() {
document.getElementById('sp-spi1-hex').value = document.getElementById('sp-spi1').value;
const spi2Base = parseInt(document.getElementById('sp-spi2').value, 16);
const sm = parseInt(document.getElementById('sp-spi2-sm').value, 16);
const spi2Byte = spi2Base | (sm << 5);
document.getElementById('sp-spi2-hex').value = spi2Byte.toString(16).padStart(2, '0').toUpperCase();
spInvalidate();
}
function updateSpKic() {
const idx = parseInt(document.getElementById('sp-kic-idx').value, 16);
const alg = parseInt(document.getElementById('sp-kic-alg').value, 16);
const byte = (idx << 4) | alg;
document.getElementById('sp-kic-hex').value = byte.toString(16).padStart(2, '0').toUpperCase();
updateKeyPlaceholder('kic', alg & 0x0F);
spInvalidate();
}
function updateSpKid() {
const idx = parseInt(document.getElementById('sp-kid-idx').value, 16);
const alg = parseInt(document.getElementById('sp-kid-alg').value, 16);
const byte = (idx << 4) | alg;
document.getElementById('sp-kid-hex').value = byte.toString(16).padStart(2, '0').toUpperCase();
updateKeyPlaceholder('kid', alg & 0x0F);
spInvalidate();
}
function updateKeyPlaceholder(prefix, algNib) {
const keyLen = {1: 8, 2: 16, 5: 16, 9: 24, 0xD: 8}[algNib] || 8;
const ph = algNib === 2 ? '16/24/32 bytes key (AES)' : keyLen + ' bytes key';
document.getElementById('sp-' + prefix + '-key').placeholder = ph;
}
// ===== DES / 3DES using des.js library (bundled as des-bundle.js) =====
function hexToBytes(s) {
const len = s.length / 2;
const b = new Uint8Array(len);
for (let i = 0; i < len; i++) b[i] = parseInt(s.substr(i * 2, 2), 16);
return b;
}
function bytesToHex(b) {
return Array.from(b).map(x => x.toString(16).padStart(2, '0').toUpperCase()).join('');
}
function des3Keys(keyBytes) {
const kLen = keyBytes.length;
if (kLen === 8) return [keyBytes, keyBytes, keyBytes];
if (kLen === 16) return [keyBytes.subarray(0,8), keyBytes.subarray(8,16), keyBytes.subarray(0,8)];
if (kLen === 24) return [keyBytes.subarray(0,8), keyBytes.subarray(8,16), keyBytes.subarray(16,24)];
throw new Error('Invalid key length: ' + kLen + ' bytes (expected 8, 16, or 24)');
}
function des3EncryptBlock(block, keyBytes) {
const [k1, k2, k3] = des3Keys(keyBytes);
const c1 = des.DES.create({type: 'encrypt', key: k1});
const c2 = des.DES.create({type: 'decrypt', key: k2});
const c3 = des.DES.create({type: 'encrypt', key: k3});
const out = new Uint8Array(8);
c1._update(block, 0, out, 0);
c2._update(out, 0, out, 0);
c3._update(out, 0, out, 0);
return out;
}
function des3DecryptBlock(block, keyBytes) {
const [k1, k2, k3] = des3Keys(keyBytes);
const c1 = des.DES.create({type: 'decrypt', key: k3});
const c2 = des.DES.create({type: 'encrypt', key: k2});
const c3 = des.DES.create({type: 'decrypt', key: k1});
const out = new Uint8Array(8);
c1._update(block, 0, out, 0);
c2._update(out, 0, out, 0);
c3._update(out, 0, out, 0);
return out;
}
function desEncryptBlock(block, key) {
const c = des.DES.create({type: 'encrypt', key: key});
const out = new Uint8Array(8);
c._update(block, 0, out, 0);
return out;
}
function desDecryptBlock(block, key) {
const c = des.DES.create({type: 'decrypt', key: key});
const out = new Uint8Array(8);
c._update(block, 0, out, 0);
return out;
}
function des3CbcEncrypt(data, keyBytes, iv) {
const bs = 8;
const padded = zeroPad(data, bs);
const out = new Uint8Array(padded.length);
let prev = iv;
for (let i = 0; i < padded.length; i += bs) {
const block = padded.subarray(i, i + bs);
const xored = new Uint8Array(bs);
for (let j = 0; j < bs; j++) xored[j] = block[j] ^ prev[j];
const enc = des3EncryptBlock(xored, keyBytes);
out.set(enc, i);
prev = enc;
}
return out;
}
function xorBytes(a, b) {
const len = Math.min(a.length, b.length);
const out = new Uint8Array(len);
for (let i = 0; i < len; i++) out[i] = a[i] ^ b[i];
return out;
}
function zeroPad(data, blockSize) {
const padLen = blockSize - (data.length % blockSize);
if (padLen === blockSize) return data;
const out = new Uint8Array(data.length + padLen);
out.set(data);
for (let i = data.length; i < out.length; i++) out[i] = 0x00;
return out;
}
// TS 03.48 / TS 102 225 KID CC: DES/3DES in CBC mode, zero IV, '00' padding.
// The MAC is the last 8 octets of the CBC encryption of the input.
function cbcMac(data, keyBytes) {
const bs = 8;
const padded = zeroPad(data, bs);
let prev = new Uint8Array(bs);
for (let i = 0; i < padded.length; i += bs) {
const block = padded.subarray(i, i + bs);
const xored = xorBytes(block, prev);
prev = des3EncryptBlock(xored, keyBytes);
}
return prev;
}
// ===== AES using aes-js library (bundled as aes-bundle.js) =====
function aesCbcEncrypt(data, keyBytes, iv) {
const bs = 16;
const padded = zeroPad(data, bs);
const aesCbc = new aesjs.ModeOfOperation.cbc(keyBytes, iv);
return new Uint8Array(aesCbc.encrypt(padded));
}
function aesShiftLeft1(bytes) {
const out = new Uint8Array(bytes.length);
let carry = 0;
for (let i = bytes.length - 1; i >= 0; i--) {
out[i] = (bytes[i] << 1) | carry;
carry = (bytes[i] & 0x80) ? 1 : 0;
}
return out;
}
function aesCmacSubkeys(keyBytes) {
const ecb = new aesjs.ModeOfOperation.ecb(keyBytes);
const L = new Uint8Array(ecb.encrypt(new Uint8Array(16)));
const K1 = aesShiftLeft1(L);
if (L[0] & 0x80) K1[15] ^= 0x87;
const K2 = aesShiftLeft1(K1);
if (K1[0] & 0x80) K2[15] ^= 0x87;
return [K1, K2];
}
// AES-CMAC (NIST SP 800-38B) truncated to 8 octets, matching pySim's CMAC(mac_len=8).
// Unlike the 3DES CBC-MAC above, CMAC pads internally - do NOT pre-pad the input.
function aesCmac(data, keyBytes) {
const bs = 16;
const [K1, K2] = aesCmacSubkeys(keyBytes);
const ecb = new aesjs.ModeOfOperation.ecb(keyBytes);
const n = Math.max(1, Math.ceil(data.length / bs));
const lastLen = data.length % bs;
const complete = data.length > 0 && lastLen === 0;
let prev = new Uint8Array(bs);
for (let i = 0; i < n - 1; i++) {
const block = data.subarray(i * bs, (i + 1) * bs);
const xored = xorBytes(block, prev);
prev = new Uint8Array(ecb.encrypt(xored));
}
let lastBlock;
if (complete) {
lastBlock = xorBytes(data.subarray((n - 1) * bs, n * bs), K1);
} else {
const padded = new Uint8Array(bs);
const start = (n - 1) * bs;
if (data.length > start) padded.set(data.subarray(start));
padded[data.length - start] = 0x80;
lastBlock = xorBytes(padded, K2);
}
const xored = xorBytes(lastBlock, prev);
const mac = new Uint8Array(ecb.encrypt(xored));
return mac.subarray(0, 8);
}
// ===== Secure Packet assembly =====
// TS 102 225 Table 1/2 + TS 31.115 Table 1 (SMS-PP):
// CPL(2) CHL(1) SPI(2) KIc(1) KID(1) TAR(3) CNTR(5) PCNTR(1) [RC/CC/DS] secured-data(+padding)
// CPL = octets from CHL to end incl. padding; CHL = 13 + len_sig.
// CPI (0x70) and CHI (null) are NOT part of the SMS packet data.
function genSp() {
const apduHex = (document.getElementById('sp-apdu').value || '').replace(/[^0-9a-fA-F]/g, '');
if (!apduHex) { document.getElementById('sp-result').value = 'Error: specify APDU'; return; }
const spi1 = parseInt(document.getElementById('sp-spi1').value, 16);
const spi2 = parseInt(document.getElementById('sp-spi2-hex').value, 16);
const kicHex = document.getElementById('sp-kic-hex').value;
const kidHex = document.getElementById('sp-kid-hex').value;
const tarHex = (document.getElementById('sp-tar').value || '').replace(/[^0-9a-fA-F]/g, '').padEnd(6, '0').slice(0, 6);
const cntrHex = (document.getElementById('sp-cntr').value || '').replace(/[^0-9a-fA-F]/g, '').padStart(10, '0').slice(0, 10);
const kicKeyHex = (document.getElementById('sp-kic-key').value || '').replace(/[^0-9a-fA-F]/g, '');
const kidKeyHex = (document.getElementById('sp-kid-key').value || '').replace(/[^0-9a-fA-F]/g, '');
const padByte = parseInt(document.getElementById('sp-padding').value, 16);
const kicAlg = parseInt(kicHex, 16) & 0x0F;
const kidAlg = parseInt(kidHex, 16) & 0x0F;
const kicIsAes = kicAlg === 0x02;
const kidIsAes = kidAlg === 0x02;
const ciphering = (spi1 & 0x04) !== 0;
const hasMac = (spi1 & 0x03) === 0x02;
const resultEl = document.getElementById('sp-result');
// Rel-18: AES requires a replay-protected counter (SPI1 b5b4 = 10 or 11)
if ((kicIsAes && ciphering) || (kidIsAes && hasMac)) {
const counterBits = (spi1 >> 3) & 0x03;
if (counterBits !== 2 && counterBits !== 3) {
resultEl.value = 'Error: AES requires a replay-protected counter (SPI1 counter: "higher" or "+1")';
return;
}
}
let kicKey, kidKey;
try {
if (kicKeyHex) {
kicKey = hexToBytes(kicKeyHex);
const ok = kicIsAes
? (kicKey.length === 16 || kicKey.length === 24 || kicKey.length === 32)
: (kicKey.length === 8 || kicKey.length === 16 || kicKey.length === 24);
if (!ok) {
resultEl.value = kicIsAes
? 'Error: AES KIc key must be 16, 24, or 32 bytes'
: 'Error: KIc key must be 8, 16, or 24 bytes';
return;
}
}
} catch (e) { resultEl.value = 'KIc key error: ' + e.message; return; }
try {
if (kidKeyHex) {
kidKey = hexToBytes(kidKeyHex);
const ok = kidIsAes
? (kidKey.length === 16 || kidKey.length === 24 || kidKey.length === 32)
: (kidKey.length === 8 || kidKey.length === 16 || kidKey.length === 24);
if (!ok) {
resultEl.value = kidIsAes
? 'Error: AES KID key must be 16, 24, or 32 bytes'
: 'Error: KID key must be 8, 16, or 24 bytes';
return;
}
}
} catch (e) { resultEl.value = 'KID key error: ' + e.message; return; }
if (ciphering && !kicKey) { resultEl.value = 'Error: ciphering requires KIc key'; return; }
if (hasMac && !kidKey) { resultEl.value = 'Error: MAC requires KID key'; return; }
const apdu = hexToBytes(apduHex);
const macLen = hasMac ? 8 : 0;
// padding aligns CNTR+PCNTR+RC/CC/DS+data to the cipher block size (ciphering only)
const cipherBlock = kicIsAes ? 16 : 8;
const padCnt = ciphering ? (cipherBlock - ((6 + macLen + apdu.length) % cipherBlock)) % cipherBlock : 0;
const chl = 13 + macLen;
const cpl = 14 + macLen + apdu.length + padCnt;
const packetLen = 3 + chl + apdu.length + padCnt;
const packet = new Uint8Array(packetLen);
packet[0] = (cpl >> 8) & 0xFF;
packet[1] = cpl & 0xFF;
packet[2] = chl;
packet[3] = spi1;
packet[4] = spi2;
packet[5] = parseInt(kicHex, 16);
packet[6] = parseInt(kidHex, 16);
packet[7] = parseInt(tarHex.substr(0,2), 16);
packet[8] = parseInt(tarHex.substr(2,2), 16);
packet[9] = parseInt(tarHex.substr(4,2), 16);
packet[10] = parseInt(cntrHex.substr(0,2), 16);
packet[11] = parseInt(cntrHex.substr(2,2), 16);
packet[12] = parseInt(cntrHex.substr(4,2), 16);
packet[13] = parseInt(cntrHex.substr(6,2), 16);
packet[14] = parseInt(cntrHex.substr(8,2), 16);
packet[15] = padCnt;
const macOff = 16;
const dataOff = macOff + macLen;
packet.set(apdu, dataOff);
for (let i = 0; i < padCnt; i++) packet[dataOff + apdu.length + i] = padByte;
if (hasMac && kidKey) {
// CC is computed over CPL+CHL+SPI+KIc+KID+TAR+CNTR+PCNTR+data(+padding),
// i.e. everything up to (but excluding) the RC/CC/DS field (TS 31.115 4.2).
const macInput = new Uint8Array(packet.length - macLen);
macInput.set(packet.subarray(0, macOff));
macInput.set(packet.subarray(dataOff), macOff);
const mac = kidIsAes ? aesCmac(macInput, kidKey) : cbcMac(macInput, kidKey);
packet.set(mac, macOff);
}
if (ciphering && kicKey) {
// ciphering covers CNTR+PCNTR+RC/CC/DS+secured data (TS 102 225 Table 2 note 1)
const toEncrypt = packet.subarray(10, packetLen);
const encrypted = kicIsAes
? aesCbcEncrypt(toEncrypt, kicKey, new Uint8Array(16))
: des3CbcEncrypt(toEncrypt, kicKey, new Uint8Array(8));
packet.set(encrypted, 10);
}
// pySim encode_cmd parity: the 2-byte CPL prefix is transmitted only when
// ciphering is applied; unciphered packets start at CHL (v1.9.5).
resultEl.value = bytesToHex(ciphering ? packet : packet.subarray(2));
}
// ===== BER-TLV =====
const BER_TAGS = {'c-apdu':'22','immediate':'81','error':'82','chaining':'83'};
const BER_QUAL = {
refresh: [
['00','NAA Initialization and Full File Change Notification'],
['01','File Change Notification'],
['02','NAA Initialization and File Change Notification'],
['03','NAA Initialization'],
['04','UICC Reset'],
['05','NAA Application Reset (not for 2G SIM)'],
['06','NAA Session Reset (not for 2G SIM)'],
['07','Steering of Roaming'],
['08','Steering of Roaming for I-WLAN'],
['09','eUICC Profile State Change (eUICC only)'],
['0A','Application Update (eUICC only)']
],
display: [
['00','Normal priority'],
['01','High priority'],
['80','Wait for user to clear message']
],
tone: [
['00','Use of vibrate alert up to the terminal'],
['01','Vibrate alert, if available, with the tone']
]
};
const BER_TONES = [
['01','Dial tone'],['02','Called subscriber busy'],['03','Congestion'],
['04','Radio path acknowledge'],['05','Radio path not available'],
['06','Error/Special info'],['07','Call waiting'],['08','Ringing tone'],
['10','General beep'],['11','Positive acknowledgement'],['12','Negative acknowledgement']
];
const BER_DEVICES = [['81','UICC'],['82','Terminal'],['83','Network'],['02','Display']];
function addBerRow(type) {
const c = document.getElementById('ber-rows');
const idx = c.children.length;
const row = document.createElement('div');
row.className = 'ber-row border border-gray-300 dark:border-slate-600 rounded p-2 bg-gray-50 dark:bg-slate-800';
row.dataset.berIdx = idx;
row.innerHTML = berRowHtml(idx, type);
c.appendChild(row);
onBerTypeChange(row.querySelector('.ber-type'));
}
function berRowHtml(idx, type) {
const types = ['c-apdu','immediate','error','chaining'].map(t =>
`<option value="${t}"${t===type?' selected':''}>${t==='c-apdu'?'C-APDU':t==='immediate'?'Immediate':t==='error'?'Error':'Chaining'}</option>`
).join('');
return `<div class="flex gap-2 items-start mb-1">
<span class="text-xs text-gray-500 dark:text-slate-400 w-6 pt-2">${idx+1}</span>
<select class="ber-type text-xs w-24 border border-gray-300 dark:border-slate-600 rounded px-1 py-1 dark:bg-slate-700 dark:text-slate-300" onchange="onBerTypeChange(this)">
${types}
</select>
<div class="ber-body flex-1"></div>
<button onclick="removeBerRow(this)" class="text-red-500 hover:text-red-700 text-sm px-1 pt-2">✕</button>
</div>
<div class="ber-preview text-xs text-gray-400 dark:text-slate-500 font-mono ml-8" data-ber-preview="${idx}"></div>`;
}
function removeBerRow(btn) {
btn.closest('.ber-row').remove();
document.querySelectorAll('.ber-row').forEach((r,i)=>r.dataset.berIdx=i);
document.querySelectorAll('.ber-row .ber-preview').forEach((p,i)=>p.dataset.berPreview=i);
}
function onBerTypeChange(sel) {
const row = sel.closest('.ber-row');
const body = row.querySelector('.ber-body');
const type = sel.value;
if (type === 'c-apdu') {
body.innerHTML = `<input class="ber-hex w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-1 py-1 dark:bg-slate-700 dark:text-slate-300" placeholder="hex" oninput="updateBerRow(this)">`;
} else if (type === 'chaining') {
body.innerHTML = buildScriptChainingRow(row.dataset.berIdx);
} else if (type === 'error') {
body.innerHTML = buildErrorActionRow();
const actionType = body.querySelector('.error-action-type');
onErrorActionTypeChange(actionType);
updateBerRow(sel);
} else {
body.innerHTML = `<div class="flex gap-1 mb-1 flex-wrap">
<select class="ber-subtype text-xs border border-gray-300 dark:border-slate-600 rounded px-1 py-1 dark:bg-slate-700 dark:text-slate-300" onchange="onBerSubTypeChange(this)">
<option value="action">Action indicator</option>
<option value="proactive">Proactive command</option>
<option value="custom">Custom hex</option>
</select>
<span class="ber-sub-body flex-1"></span>
</div>`;
onBerSubTypeChange(body.querySelector('.ber-subtype'));
}
updateBerRow(sel);
}
function buildErrorActionRow() {
return `<div class="flex gap-2 items-center">
<span class="text-xs text-gray-600 dark:text-slate-400">Error Action:</span>
<select class="error-action-type text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-700 dark:text-slate-300" onchange="onErrorActionTypeChange(this)">
<option value="proactive" selected>Proactive command (DISPLAY TEXT / PLAY TONE)</option>
<option value="noaction">No action</option>
<option value="reference">Reference to EFRMA record</option>
<option value="custom">Custom hex</option>
</select>
</div>
<div class="error-action-body mt-2"></div>`;
}
function onErrorActionTypeChange(sel) {
const body = sel.parentElement.nextElementSibling;
if (sel.value === 'proactive') {
body.innerHTML = buildBerPcHtml(['display','tone']);
const pc = body.querySelector('.ber-pc');
berPopulateDevices(pc);
onBerPcTypeChange(pc.querySelector('.ber-pc-type'));
} else if (sel.value === 'noaction') {
body.innerHTML = '<span class="text-xs text-gray-400 dark:text-slate-500">Emits <code class="font-mono">82 00</code> (no action)</span>';
} else if (sel.value === 'reference') {
body.innerHTML = `<div class="flex gap-2 items-center">
<span class="text-xs text-gray-600 dark:text-slate-400">EFRMA record ref:</span>
<input class="error-ref font-mono text-xs w-10 border border-gray-300 dark:border-slate-600 rounded px-1 py-1 dark:bg-slate-700 dark:text-slate-300" maxlength="2" placeholder="01" oninput="updateBerRow(this)">
<span class="text-xs text-gray-400">(01-7F)</span>
</div>`;
} else {
body.innerHTML = `<input class="ber-hex w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-1 py-1 dark:bg-slate-700 dark:text-slate-300" placeholder="hex" oninput="updateBerRow(this)">`;
}
const row = sel.closest('.ber-row');
if (row) updateBerRow(sel);
}
function buildBerPcHtml(allowed = ['refresh','display','tone']) {
const typeOpts = {refresh:'REFRESH', display:'DISPLAY TEXT', tone:'PLAY TONE'};
return `<div class="ber-pc">
<div class="flex gap-1 mb-1 flex-wrap">
<select class="ber-pc-type text-xs border border-gray-300 dark:border-slate-600 rounded px-1 py-1 dark:bg-slate-700 dark:text-slate-300" onchange="onBerPcTypeChange(this)">
${allowed.map(t => `<option value="${t}">${typeOpts[t]}</option>`).join('')}
</select>
<span class="text-xs pt-1">#</span>
<input class="ber-pc-num text-xs w-10 border border-gray-300 dark:border-slate-600 rounded px-1 py-1 dark:bg-slate-700 dark:text-slate-300 font-mono" type="number" min="1" max="255" value="1" onchange="updateBerRow(this)">
<span class="text-xs pt-1">Qual:</span>
<select class="ber-pc-qual text-xs border border-gray-300 dark:border-slate-600 rounded px-1 py-1 dark:bg-slate-700 dark:text-slate-300 font-mono" onchange="updateBerRow(this)"></select>
</div>
<div class="flex gap-1 mb-1 flex-wrap items-center">
<span class="text-xs">IDs:</span>
<select class="ber-pc-src text-xs border border-gray-300 dark:border-slate-600 rounded px-1 py-1 dark:bg-slate-700 dark:text-slate-300 font-mono" onchange="updateBerRow(this)"></select>
<span class="text-xs">→</span>
<select class="ber-pc-dst text-xs border border-gray-300 dark:border-slate-600 rounded px-1 py-1 dark:bg-slate-700 dark:text-slate-300 font-mono" onchange="updateBerRow(this)"></select>
</div>
<div class="ber-pc-fields text-xs"></div>
</div>`;
}
function onBerSubTypeChange(sel) {
const body = sel.closest('.ber-sub-body') || sel.parentElement.querySelector('.ber-sub-body');
const st = sel.value;
if (st === 'action') {
body.innerHTML = `<select class="ber-action text-xs border border-gray-300 dark:border-slate-600 rounded px-1 py-1 dark:bg-slate-700 dark:text-slate-300 font-mono" onchange="updateBerRow(this)">
<option value="81">81 (Session indication)</option>
<option value="82">82 (Early response)</option>
</select>`;
} else if (st === 'custom') {
body.innerHTML = `<input class="ber-hex w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-1 py-1 dark:bg-slate-700 dark:text-slate-300" placeholder="hex" oninput="updateBerRow(this)">`;
} else {
body.innerHTML = buildBerPcHtml();
const pc = body.querySelector('.ber-pc');
berPopulateDevices(pc);
onBerPcTypeChange(pc.querySelector('.ber-pc-type'));
}
updateBerRow(sel);
}
function berPopulateDevices(pc) {
const src = pc.querySelector('.ber-pc-src');
const dst = pc.querySelector('.ber-pc-dst');
BER_DEVICES.forEach(([v,l])=>{
const o1 = document.createElement('option'); o1.value=v; o1.textContent=v+' ('+l+')';
if(v==='81') o1.selected=true; src.appendChild(o1);
const o2 = document.createElement('option'); o2.value=v; o2.textContent=v+' ('+l+')';
if(v==='82') o2.selected=true; dst.appendChild(o2);
});
}
function onBerPcTypeChange(sel) {
const pc = sel.closest('.ber-pc');
const qual = pc.querySelector('.ber-pc-qual');
const fields = pc.querySelector('.ber-pc-fields');
const type = sel.value;
qual.innerHTML = (BER_QUAL[type]||[['00','']]).map(([v,l])=>`<option value="${v}">${v} ${l}</option>`).join('');
if (type === 'display') {
fields.innerHTML = `<div class="flex gap-1 items-center">
<input class="ber-pc-text flex-1 text-xs border border-gray-300 dark:border-slate-600 rounded px-1 py-1 dark:bg-slate-700 dark:text-slate-300" placeholder="Text" oninput="updateBerRow(this)">
<select class="ber-pc-enc text-xs border border-gray-300 dark:border-slate-600 rounded px-1 py-1 dark:bg-slate-700 dark:text-slate-300" onchange="updateBerRow(this)">
<option value="gsm7">GSM 7-bit</option>
<option value="ucs2">UCS2</option>
</select>
</div>
<div class="flex gap-1 items-center mt-1">
<span class="text-xs">Alpha ID:</span>
<input class="ber-pc-alpha flex-1 text-xs border border-gray-300 dark:border-slate-600 rounded px-1 py-1 dark:bg-slate-700 dark:text-slate-300 font-mono" placeholder="hex (tag 85)" oninput="updateBerRow(this)">
</div>
<div class="flex gap-1 items-center mt-1">
<label class="flex items-center gap-1 text-xs text-gray-600 dark:text-slate-400"><input type="checkbox" class="ber-pc-dur-enable" checked onchange="updateBerRow(this)"> Duration:</label>
<select class="ber-pc-dur-unit text-xs border border-gray-300 dark:border-slate-600 rounded px-1 py-1 dark:bg-slate-700 dark:text-slate-300 font-mono" onchange="updateBerRow(this)">
<option value="00">minutes</option>
<option value="01" selected>seconds</option>
<option value="02">tenths of seconds</option>
</select>
<input class="ber-pc-dur-val text-xs w-12 border border-gray-300 dark:border-slate-600 rounded px-1 py-1 dark:bg-slate-700 dark:text-slate-300 font-mono" type="number" min="1" max="255" value="30" onchange="updateBerRow(this)">
</div>`;
} else if (type === 'tone') {
fields.innerHTML = `<div class="flex gap-1 items-center">
<span class="text-xs">Tone:</span>
<select class="ber-pc-tone text-xs border border-gray-300 dark:border-slate-600 rounded px-1 py-1 dark:bg-slate-700 dark:text-slate-300 font-mono" onchange="updateBerRow(this)"></select>
</div>`;
const tsel = fields.querySelector('.ber-pc-tone');
BER_TONES.forEach(([v,l])=>{const o=document.createElement('option');o.value=v;o.textContent=v+' '+l;tsel.appendChild(o);});
} else if (type === 'refresh') {
fields.innerHTML = `<div class="flex gap-1 items-center">
<span class="text-xs">File list (tag 92):</span>
<input class="ber-pc-flist flex-1 text-xs border border-gray-300 dark:border-slate-600 rounded px-1 py-1 dark:bg-slate-700 dark:text-slate-300 font-mono" placeholder="hex (optional)" oninput="updateBerRow(this)">
</div>`;
}
updateBerRow(sel);
}
function updateBerRow(el) {
const row = el.closest('.ber-row');
const preview = row.querySelector('.ber-preview');
const val = genBerRowValue(row);
preview.textContent = val || '(empty)';
}
function genBerRowValue(row) {
const type = row.querySelector('.ber-type').value;
const tag = BER_TAGS[type];
if (type === 'error') {
return genErrorActionValue(row, tag);
}
if (type === 'chaining') {
return genScriptChainingValue(row, tag);
}
if (type === 'c-apdu') {
const hex = (row.querySelector('.ber-hex').value||'').replace(/[^0-9a-fA-F]/g,'');
if (!hex) return '';
return tag + berLenStr(hex.length/2) + hex;
}
const st = row.querySelector('.ber-subtype').value;
if (st === 'action') {
const a = row.querySelector('.ber-action').value;
return tag + berLenStr(1) + a;
}
if (st === 'custom') {
const hex = (row.querySelector('.ber-hex').value||'').replace(/[^0-9a-fA-F]/g,'');
if (!hex) return '';
return tag + berLenStr(hex.length/2) + hex;
}
return genBerPcValue(row, tag);
}
function genErrorActionValue(row, tag) {
const actionType = row.querySelector('.error-action-type');
if (!actionType) return '';
const body = row.querySelector('.error-action-body');
if (!body) return '';
switch (actionType.value) {
case 'proactive':
return genBerPcValue(body, tag);
case 'noaction':
return tag + '00';
case 'reference': {
const ref = (body.querySelector('.error-ref').value || '').replace(/[^0-9a-fA-F]/g, '');
const refNum = parseInt(ref, 16);
if (!ref || isNaN(refNum) || refNum < 0x01 || refNum > 0x7F) return '';
return tag + '01' + refNum.toString(16).padStart(2, '0').toUpperCase();
}
default: {
const hex = (body.querySelector('.ber-hex').value || '').replace(/[^0-9a-fA-F]/g, '');
if (!hex) return '';
return tag + berLenStr(hex.length / 2) + hex;
}
}
}
function buildScriptChainingRow(rowIdx, hint = '') {
return `
<div class="space-y-2">
<div class="flex gap-4 items-center flex-wrap">
<span class="text-xs font-medium text-gray-700 dark:text-slate-300">Position:</span>
<label class="flex items-center gap-1 text-xs text-gray-600 dark:text-slate-400">
<input type="radio" name="chaining-${rowIdx}" value="first" ${hint === 'intermediary' ? '' : 'checked'} onchange="updateChainingFlags(${rowIdx})">
First Script
</label>
<label class="flex items-center gap-1 text-xs text-gray-600 dark:text-slate-400">
<input type="radio" name="chaining-${rowIdx}" value="intermediary" ${hint === 'intermediary' ? 'checked' : ''} onchange="updateChainingFlags(${rowIdx})">
Intermediary Script
</label>
<label class="flex items-center gap-1 text-xs text-gray-600 dark:text-slate-400">
<input type="checkbox" name="chaining-${rowIdx}-last" value="last" ${hint === 'last' ? 'checked' : ''} onchange="updateChainingFlags(${rowIdx})">
Also Last Script
</label>
<label class="flex items-center gap-1 text-xs text-gray-600 dark:text-slate-400">
<input type="checkbox" name="chaining-${rowIdx}-keep" value="keep" onchange="updateChainingFlags(${rowIdx})">
Keep chaining info across reset (RFM)
</label>
</div>
</div>`;
}
function updateChainingFlags(rowIdx) {
const first = document.querySelector(`input[name="chaining-${rowIdx}"][value="first"]`);
const intermediate = document.querySelector(`input[name="chaining-${rowIdx}"][value="intermediary"]`);
const lastCheckbox = document.querySelector(`input[name="chaining-${rowIdx}-last"]`);
const keepCheckbox = document.querySelector(`input[name="chaining-${rowIdx}-keep"]`);
if (first && intermediate) {
if (first.checked) intermediate.checked = false;
if (intermediate.checked) first.checked = false;
}
if (lastCheckbox) lastCheckbox.disabled = !!(first && first.checked);
if (keepCheckbox) keepCheckbox.disabled = !!(first && !first.checked);
updateBerRow(document.querySelector(`[data-ber-idx="${rowIdx}"] .ber-type`));
}
function genScriptChainingValue(row, tag) {
const rowIdx = row.dataset.berIdx;
const firstRadio = row.querySelector(`input[name="chaining-${rowIdx}"][value="first"]`);
const intermediateRadio = row.querySelector(`input[name="chaining-${rowIdx}"][value="intermediary"]`);
const lastCheckbox = row.querySelector(`input[name="chaining-${rowIdx}-last"]`);
const keepCheckbox = row.querySelector(`input[name="chaining-${rowIdx}-keep"]`);
let flags = '';
if (firstRadio && firstRadio.checked) {
flags = (keepCheckbox && keepCheckbox.checked) ? '11' : '01';
} else if (intermediateRadio && intermediateRadio.checked) {
flags = (lastCheckbox && lastCheckbox.checked) ? '03' : '02';
}
if (!flags) return '';
return tag + '01' + flags;
}
function genBerPcValue(row, tag) {
const pc = row.querySelector('.ber-pc');
const pct = pc.querySelector('.ber-pc-type').value;
const cmdNum = parseInt(pc.querySelector('.ber-pc-num').value)||1;
const qual = pc.querySelector('.ber-pc-qual').value;
const src = pc.querySelector('.ber-pc-src').value;
const dst = pc.querySelector('.ber-pc-dst').value;
let value = '8103' + cmdNum.toString(16).padStart(2,'0').toUpperCase();
const typeBytes = {refresh:'01',display:'21',tone:'20'};
value += (typeBytes[pct]||'00') + qual;
value += '8202' + src + dst;
if (pct === 'display') {
const text = pc.querySelector('.ber-pc-text').value;
const enc = pc.querySelector('.ber-pc-enc').value;
if (text) {
if (enc === 'ucs2') {
const buf = [0x08];
for (const c of text) { const cd=c.charCodeAt(0); buf.push((cd>>8)&0xFF,cd&0xFF); }
value += '8D' + berLenStr(buf.length) + buf.map(b=>b.toString(16).padStart(2,'0').toUpperCase()).join('');
} else {
try {
const septets = gsm7TextToSeptets(text);
const packed = gsm7Encode(septets);
const inner = Array.from(packed).map(b=>b.toString(16).padStart(2,'0').toUpperCase());
value += '8D' + berLenStr(inner.length + 1) + '00' + inner.join('');
} catch(e) {
value += '8D' + berLenStr(text.length + 1) + '04' + text.split('').map(c=>c.charCodeAt(0).toString(16).padStart(2,'0').toUpperCase()).join('');
}
}
}
const durEnable = pc.querySelector('.ber-pc-dur-enable');
if (durEnable && durEnable.checked) {
const durVal = parseInt(pc.querySelector('.ber-pc-dur-val').value);
if (!isNaN(durVal) && durVal >= 1 && durVal <= 255) {
const durUnit = pc.querySelector('.ber-pc-dur-unit').value;
value += '84' + berLenStr(2) + durUnit + durVal.toString(16).padStart(2,'0').toUpperCase();
}
}
const alpha = (pc.querySelector('.ber-pc-alpha').value||'').replace(/[^0-9a-fA-F]/g,'');
if (alpha) value += '85' + berLenStr(alpha.length/2) + alpha;
} else if (pct === 'tone') {
const tone = pc.querySelector('.ber-pc-tone').value;
value += '8E01' + tone;
} else if (pct === 'refresh') {
const flist = (pc.querySelector('.ber-pc-flist').value||'').replace(/[^0-9a-fA-F]/g,'');
if (flist) value += '92' + berLenStr(flist.length/2) + flist;
}
return tag + berLenStr(value.length/2) + value;
}
function berLenStr(n) {
if (n < 128) return n.toString(16).padStart(2,'0').toUpperCase();
return '81' + n.toString(16).padStart(2,'0').toUpperCase();
}
function genBerTlv() {
const fmt = document.getElementById('ber-format').value;
let content = '';
document.querySelectorAll('.ber-row').forEach(row => {
const v = genBerRowValue(row);
if (v) content += v;
});
if (!content) { document.getElementById('ber-result').value = ''; return; }
let result;
if (fmt === 'AA') {
result = 'AA' + berLenStr(content.length/2) + content;
} else {
result = 'AE80' + content + '0000';
}
document.getElementById('ber-result').value = result;
document.getElementById('ber-pack-btn').disabled = false;
}
// ===== C-APDU Parser =====
const PARSE_INS = {
'A4': {name:'SELECT', data:'lc'},
'B0': {name:'READ BINARY', data:'le'},
'B2': {name:'READ RECORD(S)', data:'le'},
'DC': {name:'UPDATE RECORD', data:'lc'},
'D6': {name:'UPDATE BINARY', data:'lc'},
'0E': {name:'ERASE BINARY', data:'none'},
'44': {name:'ACTIVATE FILE', data:'none'},
'04': {name:'DEACTIVATE FILE', data:'none'},
'20': {name:'VERIFY', data:'lc'},
'24': {name:'CHANGE', data:'lc'},
'26': {name:'DISABLE PIN', data:'lc'},
'28': {name:'ENABLE PIN', data:'lc'},
'2C': {name:'UNBLOCK PIN', data:'lc'},
'E6': {name:'INSTALL', data:'lc'},
'E8': {name:'LOAD', data:'lc'},
'D4': {name:'RESIZE FILE', data:'lc'},
'E0': {name:'CREATE FILE', data:'lc'},
'E4': {name:'DELETE', data:'lc'},
'F2': {name:'GET STATUS', data:'lc'},
'CA': {name:'GET DATA', data:'none'},
'DA': {name:'PUT DATA', data:'lc'},
'CB': {name:'RETRIEVE DATA', data:'le'},
'DB': {name:'SET DATA', data:'lc'},
'A2': {name:'SEARCH RECORD', data:'lc'},
'32': {name:'INCREASE', data:'lc'},
'84': {name:'GET CHALLENGE', data:'le'},
'88': {name:'AUTHENTICATE', data:'lc'},
'89': {name:'AUTHENTICATE (odd INS)', data:'lc'},
'E2': {name:'STORE DATA', data:'lc'},
'F0': {name:'SET STATUS', data:'lc'},
'82': {name:'EXTERNAL AUTHENTICATE', data:'lc'},
'C0': {name:'GET RESPONSE', data:'le'},
'10': {name:'TERMINAL PROFILE', data:'lc'},
'C2': {name:'ENVELOPE', data:'lc'},
'12': {name:'FETCH', data:'le'},
'14': {name:'TERMINAL RESPONSE', data:'lc'},
'76': {name:'SUSPEND UICC', data:'lc'},
'AA': {name:'TERMINAL CAPABILITY', data:'lc'},
'73': {name:'MANAGE SECURE CHANNEL', data:'lc'},
'75': {name:'TRANSACT DATA', data:'lc'},
'78': {name:'GET IDENTITY', data:'le'},
'7A': {name:'EXCHANGE CAPABILITIES', data:'lc'},
'7C': {name:'MANAGE LSI', data:'lc'},
'50': {name:'INITIALIZE UPDATE', data:'lc'},
'D8': {name:'PUT KEY', data:'lc'},
'70': {name:'MANAGE CHANNEL', data:'none'},
};
function parseBerLen(hex, i) {
const b = parseInt(hex.substr(i, 2), 16);
if (b < 128) return {len: b, consumed: 2};
const n = parseInt(hex.substr(i + 2, b * 2), 16);
return {len: n, consumed: 2 + b * 2};
}
function parseTlvList(hex) {
const out = [];
let i = 0;
while (i + 4 <= hex.length) {
const tag = hex.substr(i, 2);
let tagBytes = 2;
if ((parseInt(tag, 16) & 0x1F) === 0x1F) {
tagBytes = 4;
if (i + 4 > hex.length) break;
}
const r = parseBerLen(hex, i + tagBytes);
const len = r.len;
const consumed = tagBytes + r.consumed;
if (i + consumed + len * 2 > hex.length) break;
const value = hex.substr(i + consumed, len * 2);
const raw = hex.substr(i, consumed + len * 2);
out.push({tag, length: len, value, raw});
i += consumed + len * 2;
}
return out;
}
function baseCompTag(tag) {
const b = parseInt(tag, 16);
return (b >= 0x80 && b <= 0x9F) ? (b & 0x7F).toString(16).padStart(2, '0').toUpperCase() : tag;
}
function decodePrivileges(hex) {
if (hex.length < 2) return '';
const names = [
['Security Domain', 'DAP Verification', 'Delegated Management', 'Card Lock', 'Card Terminate', 'Card Reset', 'CVM Management', 'Mandated DAP Verification'],
['Trusted Path', 'Authorized Management', 'Token Verification', 'Global Delete', 'Global Lock', 'Global Registry', 'Final Application', 'Global Service'],
['Receipt Generation', 'Ciphered Load File Data Block', 'Contactless Activation', 'Contactless Self-Activation'],
];
const parts = [];
for (let i = 0; i < Math.min(names.length, hex.length / 2); i++) {
const b = parseInt(hex.substr(i * 2, 2), 16);
for (let bit = 7; bit >= 0; bit--) {
const idx = 7 - bit;
if (b & (1 << bit) && names[i][idx]) parts.push(names[i][idx]);
}
}
return parts.join(', ') || 'None';
}
function gsm7Decode(octets) {
const bits = [];
for (const b of octets) {
for (let j = 0; j < 8; j++) bits.push((b >> j) & 1);
}
let out = '';
let escape = false;
for (let i = 0; i + 7 <= bits.length; i += 7) {
let septet = 0;
for (let j = 0; j < 7; j++) septet |= bits[i + j] << j;
if (escape) {
const ext = Object.entries(GSM7_EXT_MAP).find(([,v]) => v === septet);
out += ext ? ext[0] : '?';
escape = false;
} else if (septet === 0x1B) {
escape = true;
} else {
out += septet < GSM7_ALPHABET.length ? GSM7_ALPHABET[septet] : '?';
}
}
return out;
}
function tryUcs2(hex) {
if (hex.length < 4 || hex.length % 4 !== 0) return null;
let out = '';
for (let i = 0; i < hex.length; i += 4) {
const cp = parseInt(hex.substr(i, 4), 16);
if (cp < 0x20 || cp > 0x10FFFF) return null;
out += String.fromCodePoint(cp);
}
return out;
}
function uniformHighBytes(hex) {
const first = hex.substr(0, 2);
for (let i = 4; i < hex.length; i += 4) {
if (hex.substr(i, 2) !== first) return false;
}
return true;
}
function decodeTextData(hex) {
if (!hex || hex.length < 2) return '?';
const dcs = parseInt(hex.substr(0, 2), 16);
const rest = hex.substr(2);
if (dcs === 0x08) {
const ucs2 = tryUcs2(rest);
return ucs2 !== null ? ucs2 : '?';
}
if (dcs === 0x00 || dcs === 0x04) {
const full = tryUcs2(hex);
if (full !== null && hex.length >= 8 && uniformHighBytes(hex)) return full;
try {
const bytes = new Uint8Array(rest.length / 2);
for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(rest.substr(i * 2, 2), 16);
if (dcs === 0x00) {
const gsm = gsm7Decode(bytes);
if (gsm.length > 0) return gsm;
} else {
let s = '';
for (const b of bytes) { if (b < 0x20 || b > 0x7E) return '?'; s += String.fromCharCode(b); }
if (s.length > 0) return s;
}
} catch (e) {}
}
if (hex.length >= 4 && hex.length % 4 === 0) {
let allPrintable = true;
let ucs2 = '';
for (let i = 0; i < hex.length; i += 4) {
const cp = parseInt(hex.substr(i, 4), 16);
if (cp < 0x20 || cp > 0x10FFFF) { allPrintable = false; break; }
ucs2 += String.fromCodePoint(cp);
}
if (allPrintable && ucs2.length > 0) return ucs2;
}
try {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
const gsm = gsm7Decode(bytes);
if (gsm.length > 0 && !gsm.includes('\uFFFD')) return gsm;
} catch (e) {}
return '?';
}
function describeToolkit(hex) {
const children = [];
if (!hex || hex.length < 2) return children;
let isUicc = true;
if (hex.length >= 2) {
const first = parseInt(hex.substr(0, 2), 16);
if (first <= 0x0F) isUicc = false;
}
let i = 0;
if (isUicc) {
children.push({label:'Priority', hex:hex.substr(i,2), desc:parseInt(hex.substr(i,2),16).toString()}); i += 2;
children.push({label:'Timers', hex:hex.substr(i,2), desc:parseInt(hex.substr(i,2),16).toString()}); i += 2;
children.push({label:'Text length', hex:hex.substr(i,2), desc:parseInt(hex.substr(i,2),16).toString()}); i += 2;
const menus = parseInt(hex.substr(i,2), 16); i += 2;
children.push({label:'Menu entries', hex:hex.substr(i-2,2), desc:menus.toString()});
for (let m = 0; m < menus && i + 4 <= hex.length; m++) {
children.push({label:'Menu pair '+(m+1), hex:hex.substr(i,4), desc:'Pos ' + parseInt(hex.substr(i,2),16) + ', ID ' + hex.substr(i+2,2)});
i += 4;
}
children.push({label:'Channels', hex:hex.substr(i,2), desc:parseInt(hex.substr(i,2),16).toString()}); i += 2;
if (i + 2 <= hex.length) {
const mslLen = parseInt(hex.substr(i,2), 16); i += 2;
if (mslLen > 0 && i + mslLen * 2 <= hex.length) {
children.push({label:'MSL', hex:hex.substr(i-2, 2+mslLen*2), desc:'Length ' + mslLen + ' value ' + hex.substr(i, mslLen*2)});
i += mslLen * 2;
} else {
children.push({label:'MSL', hex:hex.substr(i-2,2), desc:'No MSL'});
}
}
if (i + 2 <= hex.length) {
const tarLen = parseInt(hex.substr(i,2), 16); i += 2;
if (tarLen > 0 && i + tarLen * 2 <= hex.length) {
children.push({label:'TAR', hex:hex.substr(i-2, 2+tarLen*2), desc:hex.substr(i, tarLen*2)});
i += tarLen * 2;
} else {
children.push({label:'TAR', hex:hex.substr(i-2,2), desc:'No TAR'});
}
}
if (i + 2 <= hex.length) {
children.push({label:'Max services', hex:hex.substr(i,2), desc:parseInt(hex.substr(i,2),16).toString()});
}
} else {
const adLen = parseInt(hex.substr(i,2), 16); i += 2;
if (adLen > 0 && i + adLen * 2 <= hex.length) {
children.push({label:'Access Domain', hex:hex.substr(i-2, 2+adLen*2), desc:hex.substr(i, adLen*2)});
i += adLen * 2;
} else {
children.push({label:'Access Domain', hex:hex.substr(i-2,2), desc:'Empty'});
}
children.push({label:'Priority', hex:hex.substr(i,2), desc:parseInt(hex.substr(i,2),16).toString()}); i += 2;
children.push({label:'Timers', hex:hex.substr(i,2), desc:parseInt(hex.substr(i,2),16).toString()}); i += 2;
children.push({label:'Text length', hex:hex.substr(i,2), desc:parseInt(hex.substr(i,2),16).toString()}); i += 2;
const menus = parseInt(hex.substr(i,2), 16); i += 2;
children.push({label:'Menu entries', hex:hex.substr(i-2,2), desc:menus.toString()});
for (let m = 0; m < menus && i + 4 <= hex.length; m++) {
children.push({label:'Menu pair '+(m+1), hex:hex.substr(i,4), desc:'Pos ' + parseInt(hex.substr(i,2),16) + ', ID ' + hex.substr(i+2,2)});
i += 4;
}
children.push({label:'Channels', hex:hex.substr(i,2), desc:parseInt(hex.substr(i,2),16).toString()}); i += 2;
if (i + 2 <= hex.length) {
const mslLen = parseInt(hex.substr(i,2), 16); i += 2;
if (mslLen > 0 && i + mslLen * 2 <= hex.length) {
children.push({label:'MSL', hex:hex.substr(i-2, 2+mslLen*2), desc:'Length ' + mslLen + ' value ' + hex.substr(i, mslLen*2)});
i += mslLen * 2;
} else {
children.push({label:'MSL', hex:hex.substr(i-2,2), desc:'No MSL'});
}
}
if (i + 2 <= hex.length) {
const tarLen = parseInt(hex.substr(i,2), 16); i += 2;
if (tarLen > 0 && i + tarLen * 2 <= hex.length) {
children.push({label:'TAR', hex:hex.substr(i-2, 2+tarLen*2), desc:hex.substr(i, tarLen*2)});
i += tarLen * 2;
} else {
children.push({label:'TAR', hex:hex.substr(i-2,2), desc:'No TAR'});
}
}
}
return children;
}
function describeProactiveCommand(tlvs) {
const children = [];
for (const tlv of tlvs) {
const base = baseCompTag(tlv.tag);
let desc = '';
if (base === '01') {
const cmdNum = parseInt(tlv.value.substr(0, 2), 16);
const typeByte = tlv.value.substr(2, 2);
const qual = tlv.value.substr(4, 2);
const typeKey = { '01': 'refresh', '21': 'display', '20': 'tone' }[typeByte];
const cmdName = { refresh: 'REFRESH', display: 'DISPLAY TEXT', tone: 'PLAY TONE' }[typeKey];
let typeName = 'Command #' + cmdNum;
if (typeKey && cmdName) {
const arr = BER_QUAL[typeKey];
const found = arr ? arr.find(([v]) => v === qual) : null;
if (found) typeName += ', ' + cmdName + ' qual ' + qual + ' ' + found[1];
else typeName += ', ' + cmdName + ' qual ' + qual;
} else {
typeName += ', type ' + typeByte + ' qual ' + qual;
}
desc = typeName;
} else if (base === '02') {
const src = tlv.value.substr(0, 2);
const dst = tlv.value.substr(2, 2);
const srcName = (BER_DEVICES.find(([v]) => v === src) || [])[1] || src;
const dstName = (BER_DEVICES.find(([v]) => v === dst) || [])[1] || dst;
desc = 'from ' + srcName + ' to ' + dstName;
} else if (base === '0D' || base === '8D') {
desc = decodeTextData(tlv.value);
} else if (base === '04' || base === '84') {
if (tlv.value.length >= 4) {
const unit = ['minutes','seconds','tenths'][parseInt(tlv.value.substr(0,2), 16)] || '?';
const val = parseInt(tlv.value.substr(2,2), 16);
desc = val + ' ' + unit;
}
} else if (base === '05' || base === '85') {
desc = 'Alpha ID: ' + tlv.value;
} else if (base === '0E' || base === '8E') {
const tone = tlv.value.substr(0, 2);
const toneName = (BER_TONES.find(([v]) => v === tone) || [])[1] || tone;
desc = 'Tone: ' + toneName;
} else if (base === '12' || base === '92') {
desc = 'File list: ' + tlv.value;
} else {
desc = tlv.value || '(empty)';
}
children.push({label:'TLV ' + tlv.tag + ' (' + baseCompTag(tlv.tag) + ')', hex:tlv.raw, desc});
}
return children;
}
function parseBerScript(hex) {
const rows = [];
let i = 0;
if (hex.startsWith('AA')) {
const r = parseBerLen(hex, 2);
i = 2 + r.consumed;
} else if (hex.startsWith('AE80')) {
i = 4;
} else {
return rows;
}
while (i < hex.length) {
if (hex.startsWith('0000', i)) break;
if (i + 4 > hex.length) break;
const tag = hex.substr(i, 2);
const r = parseBerLen(hex, i + 2);
const len = r.len;
const consumed = 2 + r.consumed;
if (i + consumed + len * 2 > hex.length) break;
const value = hex.substr(i + consumed, len * 2);
const raw = hex.substr(i, consumed + len * 2);
let label = 'Row ' + (rows.length + 1);
let children = [];
if (tag === '22') {
label = 'C-APDU';
children = parseCompactApdus(value);
} else if (tag === '81') {
label = 'Immediate Action';
children = parseActionRow(value, '81');
} else if (tag === '82') {
label = 'Error Action';
children = parseActionRow(value, '82');
} else if (tag === '83') {
label = 'Script Chaining';
children = [{label:'Flags', hex:value.substr(0,2), desc:parseChainingFlags(value.substr(0,2))}];
if (value.length > 2) children.push({label:'Script ID', hex:value.substr(2), desc:value.substr(2)});
} else {
label = 'Row ' + (rows.length + 1) + ' (tag ' + tag + ')';
}
rows.push({label, hex:raw, desc:'', children});
i += consumed + len * 2;
}
if (i < hex.length && !hex.startsWith('0000', i)) {
rows.push({label:'Trailing (unrecognized)', hex:hex.substr(i), desc:hex.substr(i)});
}
return rows;
}
function parseChainingFlags(hex) {
if (hex === '01') return 'First';
if (hex === '11') return 'First, keep across reset';
if (hex === '02') return 'Intermediary (more to follow)';
if (hex === '03') return 'Intermediary (last)';
return hex;
}
function parseActionRow(hex, tag) {
if (!hex || hex === '00') return [{label:'No action', hex:hex||'', desc:tag === '82' ? 'No action (82 00)' : ''}];
if (hex.length === 2) {
const val = parseInt(hex, 16);
if (val >= 0x01 && val <= 0x7F) {
return [{label:'Reference to EFRMA record', hex, desc:'Record 0x' + hex}];
}
const names = {'81':'Proactive session indication','82':'Early response'};
return [{label:'Action indicator', hex, desc:names[hex] || 'Value ' + hex}];
}
const tlvs = parseTlvList(hex);
if (tlvs.length > 0) {
const children = describeProactiveCommand(tlvs);
return [{label:'Proactive command', hex, desc:'', children}];
}
return [{label:'Raw', hex, desc:hex}];
}
function describeP1P2(ins, p1, p2) {
const v1 = parseInt(p1, 16);
const v2 = parseInt(p2, 16);
let d1 = '', d2 = '';
if (ins === 'A4') {
d1 = {'00':'by FID','01':'child DF','03':'parent DF','04':'by DF name (AID)','08':'path from MF','09':'path from current DF'}[p1] || '';
const resp = (v2 >> 2) & 0x07;
if (resp === 0x01) d2 = 'return FCP template';
else if (resp === 0x03) d2 = 'no response data';
if (v1 === 0x04) {
const occName = {'00':'first or only occurrence','01':'last occurrence','10':'next occurrence','11':'previous occurrence'}[(v2 & 0x03).toString(2).padStart(2,'0')];
if (occName) d2 += (d2 ? ', ' : '') + occName;
}
} else if (ins === 'B0' || ins === 'D6') {
if (v1 & 0x80) d1 = 'SFI mode, SFI ' + (v1 & 0x1F);
else d1 = 'offset high';
d2 = 'offset low';
} else if (ins === 'B2' || ins === 'DC') {
const modeKey = (v2 & 0x07).toString(16).padStart(2, '0');
d2 = {'02':'next record','03':'previous record','04':'absolute/current'}[modeKey] || '';
const sfi = (v2 >> 3) & 0x1F;
if (sfi) d2 += (d2 ? ', ' : '') + 'SFI ' + sfi;
if ((v2 & 0x07) === 0x02 || (v2 & 0x07) === 0x03) d1 = v1 === 0 ? 'ignored' : 'record # (ignored)';
else if (v1 > 0) d1 = 'record #' + v1;
} else if (ins === 'F2') {
d1 = {'80':'ISD','40':'Applications incl. SSDs','20':'Executable Load Files','10':'ELFs and Modules'}[p1] || '';
d2 = {'02':'first/all, TLV format','03':'next, TLV format','00':'first/all, deprecated raw','01':'next, deprecated raw'}[p2] || '';
} else if (ins === 'E6') {
const roles = [];
if (v1 & 0x02) roles.push('for load');
if (v1 & 0x04) roles.push('for install');
if (v1 & 0x08) roles.push('for make selectable');
if (v1 & 0x10) roles.push('for extradition');
if (v1 & 0x20) roles.push('for personalization');
if (v1 & 0x40) roles.push('for registry update');
if (roles.length) d1 = roles.join(' + ');
if (v1 & 0x80) d1 = 'more INSTALL commands follow; ' + d1;
} else if (ins === 'F0') {
d1 = {'80':'ISD','40':'Application or SSD','60':'SD + associated apps'}[p1] || '';
if (p1 === '80') {
d2 = {'01':'OP_READY','07':'INITIALIZED','0F':'SECURED','7F':'CARD_LOCKED','FF':'TERMINATED'}[p2] || '';
} else {
d2 = {'00':'unlock (previous state)','80':'LOCKED'}[p2] || '';
}
} else if (ins === '20' || ins === '24') {
if (v2 >= 1 && v2 <= 8) d2 = 'PIN ref ' + v2;
}
return {d1, d2};
}
const RAM_PARAM_TAGS = {
'C9': 'Application Specific Parameters',
'C6': 'Volatile memory quotas',
'C7': 'Non-volatile memory quotas',
'C8': 'Non-volatile memory quotas (persistent)',
'CB': 'Global Services parameters',
'CF': 'Implicit selection parameters',
'B6': 'Token CRT',
'EF': 'System Specific Parameters (SIM)',
'EA': 'System Specific Parameters (UICC)',
};
function readLvField(hex, i) {
const r = parseBerLen(hex, i);
const total = r.consumed + r.len * 2;
if (i + total > hex.length) return null;
return {value: hex.substr(i + r.consumed, r.len * 2), consumed: total, lenHex: hex.substr(i + 1, r.consumed - 2)};
}
function walkInstallParams(value) {
const tlvs = parseTlvList(value);
if (tlvs.length === 0) return null;
return tlvs.map(tlv => {
const name = RAM_PARAM_TAGS[tlv.tag];
let children = null;
if ((tlv.tag === 'EA' || tlv.tag === 'EF') && tlv.value.length >= 4) {
children = parseTlvList(tlv.value).map(inner => ({
label: 'TLV ' + inner.tag + (inner.tag === '80' ? ' (Toolkit parameters)' : inner.tag === 'CA' ? ' (SIM Toolkit parameters)' : ''),
hex: inner.raw, desc: inner.value || '(empty)',
}));
if (!children.length) children = [{label:'Value', hex:tlv.value, desc:tlv.value}];
}
const node = {label: name ? 'Tag ' + tlv.tag + ' — ' + name : 'TLV ' + tlv.tag, hex: tlv.raw, desc: tlv.value || '(empty)'};
if (children) node.children = children;
return node;
});
}
function describeInstallDataLv(p1val, data) {
let fields;
if (p1val & 0x40) {
fields = ['SD AID', '(reserved)', 'Application AID', 'Privileges', 'RU parameters', 'RU token'];
} else if (p1val & 0x20) {
fields = ['(reserved)', '(reserved)', 'Application AID', 'Privileges', 'Personalization parameters', 'Personalization token'];
} else if (p1val & 0x10) {
fields = ['Target SD AID', '(reserved)', 'Application/ELF AID', '(reserved)', 'Extradition parameters', 'Extradition token'];
} else if ((p1val & 0x08) && !(p1val & 0x04)) {
fields = ['(reserved)', '(reserved)', 'Application AID', 'Privileges', 'MS parameters', 'MS token'];
} else if (p1val & 0x04) {
fields = ['ELF AID', 'Module AID', 'Application AID', 'Privileges', 'Install parameters', 'Install token'];
} else if (p1val & 0x02) {
fields = ['Load File AID', 'SD AID', 'LFDB hash', 'Load parameters', 'Load token'];
} else {
return null;
}
const nodes = [];
let i = 0;
for (const name of fields) {
if (i >= data.length) return null;
const f = readLvField(data, i);
if (!f) return null;
const node = {label: name, hex: f.lenHex + f.value, desc:''};
if (name === 'Privileges') node.desc = decodePrivileges(f.value);
else if (name.endsWith('parameters')) {
const walked = walkInstallParams(f.value);
node.children = walked || [{label:'Value', hex:f.value, desc:f.value}];
node.desc = f.value ? '' : '(empty)';
} else if (f.value) {
node.desc = f.value;
} else {
node.desc = '(empty)';
}
nodes.push(node);
i += f.consumed;
}
if (i !== data.length) return null;
return nodes;
}
function parseOneApdu(hex) {
const isActivateDeactivate = hex.length >= 4 && (hex.substr(2, 2) === '44' || hex.substr(2, 2) === '04');
if (hex.length < 10 && !(isActivateDeactivate && hex.length === 8)) return null;
const cla = hex.substr(0, 2);
const ins = hex.substr(2, 2);
const p1 = hex.substr(4, 2);
const p2 = hex.substr(6, 2);
const p3 = hex.substr(8, 2);
const insInfo = PARSE_INS[ins];
const labels = describeP1P2(ins, p1, p2);
const children = [
{label:'CLA', hex:cla, desc:cla === '80' ? 'GP/RAM' : cla === 'A0' ? 'SIM' : parseInt(cla,16) >= 0x84 && parseInt(cla,16) <= 0x87 ? 'GlobalPlatform (secure messaging)' : 'UICC'},
{label:'INS', hex:ins, desc:insInfo ? insInfo.name : 'Unknown'},
{label:'P1', hex:p1, desc:labels.d1},
{label:'P2', hex:p2, desc:labels.d2},
];
if (isActivateDeactivate && hex.length === 8) {
return {label:'APDU (case 1)', hex:hex, desc:'no data, no Le', children};
}
if (ins === 'CA') {
if (hex.length === 10) {
children.push({label:'Le', hex:p3, desc:'Expected length: ' + parseInt(p3, 16)});
return {label:'APDU', hex:hex, desc:'', children};
}
const lc = parseInt(p3, 16);
children.push({label:'Lc', hex:p3, desc:'Data length: ' + lc});
const data = hex.substr(10, lc * 2);
children.push({label:'Data', hex:data, desc:data.startsWith('5C') && data.length >= 4 ? 'Tag list: ' + data : data});
const restLen = hex.length - (10 + lc * 2);
if (restLen >= 2) children.push({label:'Le', hex:hex.substr(10 + lc * 2, 2), desc:'Expected length: ' + parseInt(hex.substr(10 + lc * 2, 2), 16)});
return {label:'APDU', hex:hex, desc:'', children};
}
if (insInfo && insInfo.data === 'le') {
children.push({label:'Le', hex:p3, desc:'Expected length: ' + parseInt(p3, 16)});
} else if (isActivateDeactivate && hex.length >= 10) {
if (parseInt(p3, 16) === 0 && hex.length === 10) {
children.push({label:'P3', hex:p3, desc:'empty Lc (legacy form)'});
} else {
const alc = parseInt(p3, 16);
children.push({label:'Lc', hex:p3, desc:'Data length: ' + alc});
if (alc > 0 && hex.length >= 10 + alc * 2) {
const adata = hex.substr(10, alc * 2);
const adesc = adata.length === 4 ? 'FID ' + adata : (adata.length > 4 && adata.length % 2 === 0 ? 'Path ' + adata : adata);
children.push({label:'Data', hex:adata, desc:adesc});
}
}
} else if (insInfo && insInfo.data === 'none') {
children.push({label:'P3', hex:p3, desc:'No data'});
} else {
const lc = parseInt(p3, 16);
children.push({label:'Lc', hex:p3, desc:'Data length: ' + lc});
if (lc > 0 && hex.length >= 10 + lc * 2) {
const data = hex.substr(10, lc * 2);
let dataDesc = data;
let dataChildren = null;
if (ins === 'E6') {
const lv = describeInstallDataLv(parseInt(p1, 16), data);
if (lv) { dataDesc = ''; dataChildren = lv; }
else dataDesc = data;
} else if (ins === 'F0') {
if (p1 === '80') dataDesc = 'ignored for ISD';
else if (data.startsWith('4F') && data.length >= 4 && 2 + 2 + parseInt(data.substr(2, 2), 16) * 2 === data.length) dataDesc = 'Legacy 4F-TLV AID: ' + data.substr(4);
else dataDesc = 'AID (raw): ' + data;
}
const node = {label:'Data', hex:data, desc:dataDesc};
if (dataChildren) node.children = dataChildren;
children.push(node);
if (hex.length > 10 + lc * 2) {
const le = hex.substr(10 + lc * 2, 2);
children.push({label:'Le', hex:le, desc:'Expected response length: ' + parseInt(le, 16)});
}
}
}
const totalLen = hex.length;
return {label:'APDU', hex:hex.substr(0, totalLen), desc:'', children};
}
function parseCompactApdus(hex) {
const apdus = [];
let i = 0;
let prevCla = '';
while (i + 4 <= hex.length) {
const first = hex.substr(i, 2);
const second = hex.substr(i + 2, 2);
let fullCla = '';
let fullIns = '';
let consumed = 0;
const fb = parseInt(first, 16);
const knownCla = first === '80' || first === 'A0' || first === '00' || (fb >= 0x84 && fb <= 0x87);
if (knownCla && PARSE_INS[second]) {
fullCla = first;
fullIns = second;
consumed = 2;
} else if (PARSE_INS[first] && prevCla && i > 0) {
fullCla = prevCla;
fullIns = first;
consumed = 0;
}
if (fullCla) {
const p3Byte = parseInt(hex.substr(i + 6 + consumed, 2), 16);
const dataType = PARSE_INS[fullIns].data;
let apduLen = 8;
if (fullIns === 'CA') {
const remAvail = hex.length - (i + consumed);
if (p3Byte > 0 && remAvail >= p3Byte * 2 + 2) apduLen += p3Byte * 2 + 2;
} else if (fullIns === '44' || fullIns === '04') {
const avail = hex.length - i;
const p3Hex = avail >= consumed + 8 ? hex.substr(i + 6 + consumed, 2) : null;
if (p3Hex !== null) {
if (p3Byte > 0) apduLen += p3Byte * 2;
} else if (avail >= consumed + 6) {
apduLen -= 2;
}
} else if (dataType === 'lc') {
apduLen += p3Byte * 2;
}
if (i + consumed + apduLen <= hex.length) {
const apduHex = fullCla + hex.substr(i + consumed, apduLen);
const parsed = parseOneApdu(apduHex);
if (parsed) {
prevCla = fullCla;
parsed.hex = hex.substr(i, consumed + apduLen);
parsed.label = consumed === 0 ? 'APDU (implied CLA)' : 'APDU';
apdus.push(parsed);
i += consumed + apduLen;
continue;
}
}
}
apdus.push({label:'Unknown', hex:hex.substr(i, 2), desc:'Unrecognized'});
i += 2;
}
if (i < hex.length) {
if (apdus.length > 0 && hex.length - i === 2) {
const leHex = hex.substr(i);
apdus.push({label:'Le', hex:leHex, desc:'Expected response length: ' + parseInt(leHex, 16)});
} else {
apdus.push({label:'Trailing (unrecognized)', hex:hex.substr(i), desc:hex.substr(i)});
}
}
return apdus;
}
function parseHexTree(hex) {
const s = hex.replace(/[^0-9a-fA-F]/g, '').toUpperCase();
if (s.length % 2 !== 0) return {label:'Error', hex:'', desc:'Odd hex length'};
if (s.startsWith('AA') && s.length >= 4) {
const rows = parseBerScript(s);
return {label:'Expanded Script (AA)', hex:s, desc:'', children:rows};
}
if (s.startsWith('AE80')) {
const rows = parseBerScript(s);
return {label:'Expanded Script (AE80)', hex:s, desc:'', children:rows};
}
const apdus = parseCompactApdus(s);
return {label:'Compact C-APDU chain', hex:s, desc:'', children:apdus};
}
function renderTree(node, container) {
const div = document.createElement('div');
div.className = 'ml-3 mb-1';
const header = document.createElement('div');
header.className = 'flex gap-2 items-start py-0.5 cursor-pointer hover:bg-gray-100 dark:hover:bg-slate-700 rounded';
if (node.children && node.children.length > 0) {
const toggle = document.createElement('span');
toggle.textContent = '▾';
toggle.className = 'text-xs text-gray-400 w-4 flex-shrink-0';
header.appendChild(toggle);
header.onclick = () => {
const body = div.querySelector('.tree-body');
if (body) {
body.classList.toggle('hidden');
toggle.textContent = body.classList.contains('hidden') ? '▸' : '▾';
}
};
} else {
const dot = document.createElement('span');
dot.textContent = '·';
dot.className = 'text-xs text-gray-300 w-4 flex-shrink-0';
header.appendChild(dot);
}
const label = document.createElement('span');
label.className = 'text-sm font-medium text-gray-700 dark:text-slate-300';
label.textContent = node.label;
header.appendChild(label);
if (node.hex) {
const hex = document.createElement('span');
hex.className = 'text-sm font-mono text-indigo-600 dark:text-indigo-400 ml-2';
hex.textContent = node.hex;
header.appendChild(hex);
}
if (node.desc) {
const desc = document.createElement('span');
desc.className = 'text-sm text-gray-600 dark:text-slate-400 ml-2';
desc.textContent = node.desc;
header.appendChild(desc);
}
div.appendChild(header);
if (node.children && node.children.length > 0) {
const body = document.createElement('div');
body.className = 'tree-body';
div.appendChild(body);
for (const child of node.children) {
renderTree(child, body);
}
}
container.appendChild(div);
}
function parseHex() {
const input = document.getElementById('parse-input');
const output = document.getElementById('parse-output');
const hex = input.value.replace(/[^0-9a-fA-F]/g, '').toUpperCase();
if (!hex || hex.length % 2 !== 0) {
output.innerHTML = '<div class="text-xs text-red-500">Invalid hex input</div>';
return;
}
const tree = parseHexTree(hex);
output.innerHTML = '';
renderTree(tree, output);
}
// ===== Response Parser =====
const SW_MAP = {
generic: {
'9000': 'Normal ending of command',
'61XX': 'Send GET RESPONSE (Le = SW2)',
'6CXX': 'Repeat command with Le = SW2',
// TS 51.011 §9.4 SIM-specific families
'9200': 'Command successful after internal update retry routine',
'9240': 'Memory problem',
'9400': 'No EF selected',
'9402': 'Out of range (invalid address)',
'9404': 'File ID not found / pattern not found',
'9408': 'File inconsistent with the command',
'9802': 'No CHV initialized',
'9804': 'Access condition not fulfilled / CHV verification failed, attempts left / authentication failed',
'9808': 'In contradiction with CHV status',
'9810': 'In contradiction with invalidation status',
'9840': 'CHV verification failed - no attempt left; CHV blocked; UNBLOCK blocked',
'9850': 'Increase cannot be performed, max value reached',
'9EXX': 'SIM data download error (SW2 = length)',
'9FXX': 'Response data available (SW2 = N bytes) - send GET RESPONSE',
'6200': 'Warning: No information given, NV memory unchanged',
'6281': 'Warning: Part of returned data may be corrupted',
'6282': 'Warning: End of file/record reached before reading Le bytes',
'6283': 'Warning: Selected file invalidated',
'6285': 'Warning: Selected file in termination state',
'62F1': 'Warning: More data available',
'62F2': 'Warning: More data available and proactive command pending',
'62F3': 'Warning: Response data available',
'63F1': 'Warning: More data expected',
'63F2': 'Warning: More data expected and proactive command pending',
'63CX': 'Verification failed, X retries remaining (SW2 & 0x0F = tries; X=0: no counter provided / blocked for VERIFY)',
'6400': 'No information given, state of NV memory unchanged',
'6500': 'No information given, state of NV memory changed',
'6581': 'Memory problem',
'6700': 'Wrong length in Lc',
'6800': 'Checking error: No information given (CLA functions)',
'6881': 'Logical channel not supported',
'6882': 'Secure messaging not supported',
'6900': 'No information given',
'6981': 'Command incompatible with file structure',
'6982': 'Security status not satisfied',
'6983': 'Authentication/PIN method blocked',
'6984': 'Referenced data invalidated',
'6985': 'Conditions of use not satisfied',
'6986': 'Command not allowed (no EF selected)',
'6989': 'Command not allowed — secure channel — security not satisfied',
'6A80': 'Incorrect parameters in the data field',
'6A81': 'Function not supported',
'6A82': 'File not found',
'6A83': 'Record not found',
'6A84': 'Not enough memory space',
'6A86': 'Incorrect parameters P1-P2',
'6A87': 'Lc inconsistent with P1-P2',
'6A85': 'Nc inconsistent with TLV data object',
'6A88': 'Referenced data not found',
'6A89': 'File already exists',
'6A8A': 'DF name already exists',
'6B00': 'Wrong parameter(s) P1-P2',
'6D00': 'Instruction code not supported or invalid',
'6E00': 'Class not supported',
'6F00': 'Technical problem, no precise diagnosis',
'9850': 'INCREASE cannot be performed, max value reached',
'9862': 'Authentication error, application specific',
'9863': 'Security session or association expired',
'9864': 'Minimum UICC suspension time is too long',
},
gp: {
'6310': 'More data available — issue GET STATUS [get next occurrence]',
'6581': 'Memory failure',
'6A80': 'Incorrect values in command data',
'6A81': 'Function not supported (card Life Cycle State is CARD_LOCKED)',
'6A82': 'Selected Application / file not found',
'6A84': 'Not enough memory space',
'6A88': 'Referenced data not found',
'6881': 'Logical channel not supported or is not active',
'6882': 'Secure messaging not supported',
'6883': 'The last command of the chain was expected',
'9484': 'Algorithm not supported (DEPRECATED)',
'9485': 'Invalid key check value (DEPRECATED)',
'6300': 'Authentication of host cryptogram failed',
'6200': 'Warning: Logical Channel already closed',
'6283': 'Warning: Card Life Cycle State is CARD_LOCKED',
},
uicc: {
'91XX': 'Normal ending with proactive UICC command pending (SW2 = length of response data)',
'9100': 'Normal ending with proactive UICC command pending (no response data)',
'92XX': 'Normal ending with ongoing data transfer session info',
'9300': 'SIM Application Toolkit is busy',
'62F1': 'More data available (ENVELOPE, RETRIEVE DATA, SET DATA)',
'62F2': 'More data available and proactive command pending',
'62F3': 'Response data available (ENVELOPE)',
'63F1': 'More data expected (ENVELOPE, RETRIEVE DATA, SET DATA)',
'63F2': 'More data expected and proactive command pending (RETRIEVE DATA)',
'63CX': 'Verification failed, X retries remaining (VERIFY PIN, CHANGE PIN, etc.)',
'6A80': 'Incorrect parameters in the data field',
'6A82': 'File not found',
'6A83': 'Record not found (READ RECORD, SEARCH RECORD, UPDATE RECORD)',
'6A84': 'Not enough memory space (RETRIEVE DATA, SET DATA)',
'6A87': 'Lc inconsistent with P1 to P2 (SELECT only)',
'6A88': 'Referenced data not found',
'6981': 'Command incompatible with file structure',
'6983': 'Authentication/PIN method blocked',
'6984': 'Referenced data invalidated',
'6986': 'Command not allowed (no EF selected)',
'6989': 'Command not allowed — secure channel — security not satisfied',
},
};
const SW_CMD_NAMES = {
'sim-select': 'SIM / USIM — SELECT',
'sim-status': 'SIM / USIM — STATUS',
'sim-read-binary': 'SIM / USIM — READ BINARY',
'sim-update-binary': 'SIM / USIM — UPDATE BINARY',
'sim-read-record': 'SIM / USIM — READ RECORD',
'sim-update-record': 'SIM / USIM — UPDATE RECORD',
'sim-verify': 'SIM / USIM — VERIFY PIN',
'sim-change': 'SIM / USIM — CHANGE PIN',
'sim-disable': 'SIM / USIM — DISABLE PIN',
'sim-enable': 'SIM / USIM — ENABLE PIN',
'sim-unblock': 'SIM / USIM — UNBLOCK PIN',
'sim-deactivate': 'SIM / USIM — DEACTIVATE FILE',
'sim-activate': 'SIM / USIM — ACTIVATE FILE',
'sim-challenge': 'SIM / USIM — GET CHALLENGE',
'sim-auth': 'SIM / USIM — AUTHENTICATE',
'sim-profile': 'SIM / USIM — TERMINAL PROFILE',
'sim-envelope': 'SIM / USIM — ENVELOPE',
'sim-fetch': 'SIM / USIM — FETCH',
'sim-terminal-rsp': 'SIM / USIM — TERMINAL RESPONSE',
'sim-manage-channel': 'SIM / USIM — MANAGE CHANNEL',
'sim-msc': 'SIM / USIM — MANAGE SECURE CHANNEL',
'sim-retrieve': 'SIM / USIM — RETRIEVE DATA',
'sim-set-data': 'SIM / USIM — SET DATA',
'sim-suspend': 'SIM / USIM — SUSPEND UICC',
'ram-install-load': 'RAM — INSTALL [for load]',
'ram-install-install': 'RAM — INSTALL [for install]',
'ram-install-make-sel': 'RAM — INSTALL [for make selectable]',
'ram-install-reg-update': 'RAM — INSTALL [for registry update]',
'ram-install-extradition': 'RAM — INSTALL [for extradition]',
'ram-load': 'RAM — LOAD',
'ram-delete': 'RAM — DELETE',
'ram-get-status': 'RAM — GET STATUS',
'ram-get-data': 'RAM — GET DATA',
'ram-store-data': 'RAM — STORE DATA',
'ram-set-status': 'RAM — SET STATUS',
'ram-ext-auth': 'RAM — EXTERNAL AUTHENTICATE',
'ram-int-auth': 'RAM — INTERNAL AUTHENTICATE',
'ram-select': 'RAM — SELECT',
'ram-init-update': 'RAM — INITIALIZE UPDATE',
'ram-put-key': 'RAM — PUT KEY',
'ram-manage-channel': 'RAM — MANAGE CHANNEL',
};
const LIFECYCLE_MAP = {
'01': 'OP_READY (card) / LOADED (ELF)',
'03': 'INSTALLED',
'07': 'SELECTABLE',
'0F': 'SECURED (card)',
'1F': 'PERSONALIZED (SD)',
'7F': 'CARD_LOCKED',
'FF': 'TERMINATED',
'83': 'LOCKED (SD)',
'87': 'LOCKED (SD)',
'8F': 'LOCKED (SD)',
};
const PRIVILEGE_NAMES = [
['Security Domain', 'DAP Verification', 'Delegated Management', 'Card Lock', 'Card Terminate', 'Card Reset', 'CVM Management', 'Mandated DAP Verification'],
['Trusted Path', 'Authorized Management', 'Token Verification', 'Global Delete', 'Global Lock', 'Global Registry', 'Final Application', 'Global Service'],
['Receipt Generation', 'Ciphered Load File Data Block', 'Contactless Activation', 'Contactless Self-Activation'],
];
function getSwCategory(cmd) {
if (!cmd) return 'generic';
return cmd.startsWith('ram-') ? 'gp' : 'uicc';
}
function lookupSw(sw1, sw2, cat) {
const key = sw1 + sw2;
const wild = sw1 + 'XX';
const cx = sw1 + 'CX';
const maps = [SW_MAP.generic];
if (cat === 'gp' || cat === 'uicc') maps.push(SW_MAP[cat]);
if (cat === 'uicc') maps.push(SW_MAP.gp);
for (const m of maps) {
if (m[key]) return m[key];
if (m[wild]) return m[wild];
if (m[cx] && (parseInt(sw2,16) & 0xF0) === 0xC0) {
const tries = parseInt(sw2,16) & 0x0F;
return m[cx].replace('X', tries);
}
}
return '';
}
function updateRespCmd() {
const cmd = document.getElementById('resp-cmd').value;
const ctx = document.getElementById('resp-ctx');
if (!cmd) { ctx.value = 'generic'; return; }
ctx.value = getSwCategory(cmd) === 'gp' ? 'GlobalPlatform' : 'SIM / UICC';
}
function parseTLV(hex) {
const tags = [];
let i = 0;
while (i < hex.length) {
let tag = hex.substr(i, 2); i += 2;
if (tag >= '1F' && tag <= '7F') { tag += hex.substr(i, 2); i += 2; }
else if (tag >= '9F' && tag <= 'BF') { tag += hex.substr(i, 2); i += 2; }
else if (tag >= 'DF' && tag <= 'FF') { tag += hex.substr(i, 2); i += 2; }
let lenByte = parseInt(hex.substr(i, 2), 16); i += 2;
let len;
if (lenByte < 0x80) { len = lenByte; }
else if (lenByte === 0x81) { len = parseInt(hex.substr(i, 2), 16); i += 2; }
else if (lenByte === 0x82) { len = parseInt(hex.substr(i, 4), 16); i += 4; }
else { len = lenByte; }
const value = hex.substr(i, len * 2); i += len * 2;
const isConstructed = parseInt(tag, 16) >= 0xA0 && parseInt(tag, 16) < 0xC0;
const children = isConstructed ? parseTLV(value) : null;
tags.push({ tag, len, value, children, isConstructed });
}
return tags;
}
function formatTlvTree(tags, indent) {
if (!indent) indent = 0;
const pad = ' '.repeat(indent);
let html = '';
for (const t of tags) {
const tagName = TLV_TAG_NAMES[t.tag] || '';
html += pad + '<span class="text-blue-600 dark:text-blue-400">' + t.tag + '</span>';
html += ' <span class="text-gray-500">len=' + t.len + '</span>';
if (tagName) html += ' <span class="text-green-600 dark:text-green-400">// ' + tagName + '</span>';
if (t.children && t.children.length) {
html += '\n' + formatTlvTree(t.children, indent + 1);
} else {
const decoded = decodeTlvValue(t);
if (decoded) html += ' <span class="text-purple-600 dark:text-purple-400">→ ' + decoded + '</span>';
html += '\n';
}
}
return html;
}
const TLV_TAG_NAMES = {
'4F': 'Application AID',
'5F50': 'SD Manager URL',
'9F70': 'Life Cycle State',
'C5': 'Privileges',
'C4': 'Application ELF AID',
'CC': 'Associated SD AID',
'CE': 'ELF Version Number',
'84': 'Executable Module AID',
'CF': 'Implicit Selection Parameter',
'E3': 'GP Registry Data',
'42': 'Issuer Identification Number',
'45': 'Card Image Number',
'66': 'Card Data',
'67': 'Card Capability Information',
'E0': 'Key Information Template',
'C0': 'Key Information Data',
'D3': 'Current Security Level',
'C1': 'Sequence Counter',
'C2': 'Confirmation Counter',
'5C': 'Tag List',
'7F21': 'Certificate',
'5031': 'Certificate Info (EF.OD)',
'6F': 'FCI Template',
'62': 'FCP Template',
'64': 'FMD Template',
'A0': 'Application Template',
'A5': 'Proprietary FCI Data',
'81': 'Immediate Action',
'82': 'Error Action',
'22': 'C-APDU',
'83': 'Script Chaining',
};
function decodeTlvValue(t) {
if (t.tag === '9F70' && t.value.length === 2) {
return LIFECYCLE_MAP[t.value] || 'Unknown state (' + t.value + ')';
}
if (t.tag === 'C5' && t.value.length >= 2) {
return decodePrivileges(t.value);
}
if (t.tag === '4F') {
return t.value.replace(/(..)/g, '$1 ').trim();
}
if (t.tag === 'C4' || t.tag === 'CC' || t.tag === '84') {
return t.value.replace(/(..)/g, '$1 ').trim();
}
if (t.tag === 'CE' && t.value.length === 4) {
return 'v' + parseInt(t.value.substr(0,2),16) + '.' + parseInt(t.value.substr(2,2),16);
}
if (t.tag === 'D3' && t.value.length === 2) {
const v = parseInt(t.value,16);
const parts = [];
if (v & 0x80) parts.push('C-MAC');
if (v & 0x40) parts.push('MAC');
if (v & 0x20) parts.push('R-MAC');
if (v & 0x10) parts.push('R-CC');
if (v & 0x08) parts.push('C-DEC');
if (v & 0x04) parts.push('D-DEC');
if (v & 0x02) parts.push('C-ENC');
if (v & 0x01) parts.push('D-ENC');
return parts.length ? parts.join(' + ') : 'No security';
}
if (t.tag === 'C1' && t.value.length >= 2) {
return 'Seq: 0x' + t.value;
}
if (t.tag === 'C2' && t.value.length >= 2) {
return 'Counter: 0x' + t.value;
}
if (t.tag === '5F50') {
try {
const bytes = [];
for (let i = 0; i < t.value.length; i += 2) bytes.push(parseInt(t.value.substr(i,2),16));
return new TextDecoder().decode(new Uint8Array(bytes));
} catch(e) { return t.value; }
}
return '';
}
function decodePrivileges(hex) {
const bytes = [];
for (let i = 0; i < hex.length; i += 2) bytes.push(parseInt(hex.substr(i,2),16));
const names = [];
for (let b = 0; b < Math.min(bytes.length, PRIVILEGE_NAMES.length); b++) {
const byteNames = PRIVILEGE_NAMES[b] || [];
for (let bit = 0; bit < byteNames.length; bit++) {
if (bytes[b] & (0x80 >> bit)) {
if (byteNames[bit]) names.push(byteNames[bit]);
}
}
}
return names.length ? names.join(', ') : 'None';
}
function decodeResponse() {
const cmd = document.getElementById('resp-cmd').value;
const swHex = (document.getElementById('resp-sw').value || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase().padStart(4, '0').slice(0, 4);
const sw1 = swHex.slice(0, 2);
const sw2 = swHex.slice(2, 4);
const rawData = (document.getElementById('resp-data').value || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
const out = document.getElementById('resp-output');
let html = '';
if (!sw1 || !sw2) {
html += '<span class="text-red-500">Please enter SW1 and SW2</span>';
out.innerHTML = html;
return;
}
const cat = getSwCategory(cmd);
const swDesc = lookupSw(sw1, sw2, cat);
const isOk = sw1 === '90' && sw2 === '00';
html += '<div class="mb-2">';
if (isOk) {
html += '<span class="text-green-600 dark:text-green-400 font-bold">✓</span> ';
} else {
html += '<span class="text-red-500 dark:text-red-400 font-bold">✗</span> ';
}
html += 'SW1=' + sw1 + ' SW2=' + sw2;
if (cmd && SW_CMD_NAMES[cmd]) html += ' <span class="text-gray-500">(' + SW_CMD_NAMES[cmd] + ')</span>';
html += '<br>';
if (swDesc) {
html += '<span class="text-gray-700 dark:text-slate-300">' + swDesc + '</span>';
} else {
html += '<span class="text-yellow-600 dark:text-yellow-400">Unknown status word</span>';
}
html += '</div>';
if (rawData) {
html += '<div class="mt-2 pt-2 border-t border-gray-300 dark:border-slate-600">';
html += '<div class="text-sm font-medium text-gray-700 dark:text-slate-300 mb-1">Response Data</div>';
if (cmd === 'ram-get-status') {
const tlv = parseTLV(rawData);
if (tlv.length) {
html += '<pre class="text-xs leading-relaxed">' + formatTlvTree(tlv) + '</pre>';
} else {
html += '<span class="text-gray-500">' + rawData.replace(/(..)/g, '$1 ').trim() + '</span>';
}
} else if (cmd === 'ram-get-data') {
const tlv = parseTLV(rawData);
if (tlv.length) {
html += '<pre class="text-xs leading-relaxed">' + formatTlvTree(tlv) + '</pre>';
} else {
html += '<span class="text-gray-500">' + rawData.replace(/(..)/g, '$1 ').trim() + '</span>';
}
} else if (cmd === 'sim-select' || cmd === 'ram-select') {
const tlv = parseTLV(rawData);
if (tlv.length) {
html += '<pre class="text-xs leading-relaxed">' + formatTlvTree(tlv) + '</pre>';
} else {
html += '<span class="text-gray-500">' + rawData.replace(/(..)/g, '$1 ').trim() + '</span>';
}
} else {
html += '<span class="text-gray-500">' + rawData.replace(/(..)/g, '$1 ').trim() + '</span>';
}
html += '</div>';
}
out.innerHTML = html;
}
// ===== pysim integration =====
// Default to a local server on 127.0.0.1:8080. If this PWA is served by
// pysim-otaman-server itself (same origin), detect the co-located API once
// and switch to a relative base; otherwise keep the default and wait for the
// user to point at a server.
let pysimBase = 'http://127.0.0.1:8080';
async function pysimDetectSameOrigin() {
try {
const res = await fetch('/api/version', { method: 'GET', cache: 'no-store' });
if (!res.ok) return;
const data = await res.json();
if (!data || typeof data.version !== 'string') return;
pysimBase = '';
const urlEl = document.getElementById('pysim-url');
if (urlEl) { urlEl.value = ''; urlEl.placeholder = '(same origin)'; }
_pysimAutoConnectAttempted = true;
pysimConnect();
} catch (e) {
// no co-located API (file://, plain static server, SPA fallback) — keep default
}
}
pysimDetectSameOrigin();
function esc(str) {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
async function pysimFetch(path, body) {
const url = pysimBase + path;
const opts = { method: body ? 'POST' : 'GET', headers: { 'Content-Type': 'application/json', 'Accept-Language': currentLang } };
if (body) opts.body = JSON.stringify(body);
const res = await fetch(url, opts);
return await res.json();
}
function pysimTargetSpace() {
let h;
try { h = new URL(pysimBase).hostname.toLowerCase(); } catch (e) { return 'public'; }
if (h === 'localhost' || h === '[::1]' || h === '::1' || /^127\./.test(h)) return 'loopback';
if (/^10\./.test(h) || /^192\.168\./.test(h) || /^172\.(1[6-9]|2\d|3[01])\./.test(h)) return 'private';
return 'public';
}
function pysimIsLikelyBlocked() {
const pagePublic = location.protocol === 'https:'
|| !(location.hostname === 'localhost' || /^127\./.test(location.hostname));
const s = pysimTargetSpace();
return pagePublic && (s === 'loopback' || s === 'private');
}
function pysimSetConnected(state) {
const img = document.getElementById('pysim-status-img');
if (state === 'spin') {
img.src = 'sim_anim.svg';
} else {
img.src = state ? 'sim.svg' : 'nosim.svg';
}
}
function pysimShowConnected(connected) {
document.getElementById('pysim-connect-row').classList.toggle('hidden', connected);
document.getElementById('pysim-connect-info').classList.toggle('hidden', connected);
document.getElementById('pysim-connected-row').classList.toggle('hidden', !connected);
document.getElementById('pysim-body').classList.toggle('hidden', !connected);
document.getElementById('sp-verify-btn').classList.toggle('hidden', !connected);
document.getElementById('sp-send-btn').classList.toggle('hidden', !connected);
}
async function pysimRefresh() {
const statusEl = document.getElementById('pysim-status');
try {
const data = await pysimFetch('/api/status');
pysimSetConnected(true);
let html = 'Reader: <b>' + esc(data.reader || 'none') + '</b>' +
' | Card: <b>' + esc(data.card || 'none') + '</b>' +
' | Profile: <b>' + esc(data.profile || 'none') + '</b><br>' +
'ATR: <b>' + esc(data.atr || 'n/a') + '</b>' +
(data.cla_byte ? ' | CLA: <b>' + esc(data.cla_byte) + '</b>' : '') +
(data.sel_ctrl ? ' | SEL: <b>' + esc(data.sel_ctrl) + '</b>' : '') +
' | Channels: <b>' + (data.channels && data.channels.length ? data.channels.join(', ') : 'none') + '</b><br>' +
'ADM: <b>' + (data.adm_verified ? 'verified' : 'not verified') + '</b>' +
' | Selection: <b>' + (data.current_selection
? (data.current_selection.name || data.current_selection.fid || '?')
: 'none') + '</b>' +
(data.current_selection
? ' [' + (data.current_selection.type || '?') + ']' +
' FID=' + (data.current_selection.fid || '?') +
(data.current_selection.path ? ' path=' + data.current_selection.path : '') +
(data.current_selection.file_size ? ' size=' + data.current_selection.file_size : '') +
(data.current_selection.record_len ? ' rec_len=' + data.current_selection.record_len : '') +
(data.current_selection.num_of_rec ? ' rec_cnt=' + data.current_selection.num_of_rec : '')
: '');
statusEl.innerHTML = html;
return true;
} catch (e) {
pysimSetConnected(false);
statusEl.textContent = t('Connection failed:') + ' ' + e.message;
return false;
}
}
async function pysimConnect() {
const urlInput = document.getElementById('pysim-url');
pysimBase = urlInput.value.replace(/\/+$/, '');
const wasAuto = _pysimAutoConnectAttempted;
_pysimAutoConnectAttempted = false;
const btn = document.getElementById('pysim-connect-btn');
btn.disabled = true;
btn.textContent = t('Connecting...');
const statusEl = document.getElementById('pysim-status');
const msgEl = document.getElementById('pysim-connect-msg');
msgEl.innerHTML = '<span class="text-gray-500">' + t('Checking card...') + '</span>';
pysimSetConnected('spin');
try {
// Version check
const ver = await pysimFetch('/api/version');
if (!ver.version) throw new Error('No version');
const sv = (ver.version || '').split('.').map(Number);
if (sv[0] < 1 || (sv[0] === 1 && sv[1] < 0)) {
msgEl.innerHTML = '<span class="text-red-500">Server version ' + esc(ver.version) + ' is outdated. Please update pysim-otaman-server.</span>';
btn.textContent = t('Connect');
btn.disabled = false;
return;
}
if (sv[0] > 1) {
msgEl.innerHTML = '<span class="text-yellow-500">Warning: server version ' + esc(ver.version) + ' is newer than expected. Update OTAMan for full compatibility.</span>';
}
const status = await pysimFetch('/api/status');
if (!status.app_ready) {
throw new Error('Server not ready');
}
const cmd = await pysimFetch('/api/command', { cmd: 'status' });
if (cmd.output && (cmd.output.includes('Exception') || cmd.output.includes('ProtocolError'))) {
pysimSetConnected(false);
statusEl.innerHTML = '<span class="text-red-500">' + t('No card detected. Insert card and click Equip card button') + '</span>';
pysimShowConnected(true);
btn.textContent = t('Connected');
btn.disabled = false;
return;
}
pysimSetConnected(true);
pysimShowConnected(true);
await pysimRefresh();
pysimLoadCommands();
pysimFsRefresh();
pysimPollStatusInit();
pysimStartBackendPoll();
btn.textContent = t('Connected');
stkCheckMenu();
} catch (e) {
if (wasAuto) _pysimAutoConnectAttempted = true;
pysimSetConnected(false);
const hint = t(pysimIsLikelyBlocked() ? 'pysim-blocked-hint' : 'pysim-unreachable-hint')
.replace('{url}', esc(pysimBase));
msgEl.innerHTML =
'<span class="text-red-500">' + t('Connection failed:') + ' ' + esc(e.message) + '</span>' +
'<br><span class="text-sm text-gray-600 dark:text-slate-400">' + hint + '</span>';
btn.textContent = t('Connect');
}
btn.disabled = false;
}
function pysimDisconnect() {
if (_pysimPollTimer) { clearInterval(_pysimPollTimer); _pysimPollTimer = null; }
pysimShowConnected(false);
document.getElementById('pysim-status').textContent = '';
pysimSetConnected(false);
}
async function pysimRunCmd(cmd) {
const statusEl = document.getElementById('pysim-status');
try {
const result = await pysimFetch('/api/command', { cmd });
if (result.output && (result.output.includes('Exception') || result.output.includes('ProtocolError') || result.output.includes('SwMatchError'))) {
pysimSetConnected(false);
statusEl.innerHTML = '<span class="text-red-500">' + t('No card detected. Insert card and click Equip card button') + '</span>';
return null;
}
return result;
} catch (e) {
pysimSetConnected(false);
statusEl.innerHTML = '<span class="text-red-500">' + t('No card detected. Insert card and click Equip card button') + '</span>';
return null;
}
}
async function pysimStatusCmd() {
const statusEl = document.getElementById('pysim-status');
statusEl.innerHTML = '<span class="text-gray-500">' + t('Checking...') + '</span>';
pysimSetConnected('spin');
const result = await pysimRunCmd('status');
if (result) await pysimRefresh();
}
async function pysimResetCmd() {
const statusEl = document.getElementById('pysim-status');
statusEl.innerHTML = '<span class="text-gray-500">' + t('Resetting...') + '</span>';
pysimSetConnected('spin');
const result = await pysimRunCmd('reset');
if (result) {
await pysimRefresh();
pysimFsRefresh();
}
}
async function pysimEquipCmd() {
const statusEl = document.getElementById('pysim-status');
statusEl.innerHTML = '<span class="text-gray-500">' + t('Equipping...') + '</span>';
pysimSetConnected('spin');
const result = await pysimRunCmd('equip');
if (result) {
await pysimRefresh();
pysimFsRefresh();
stkCheckMenu();
}
}
async function pysimExec() {
const cmd = document.getElementById('pysim-cmd').value.trim();
if (!cmd) return;
const out = document.getElementById('pysim-cmd-output');
out.value = '> ' + cmd + '\n';
try {
const data = await pysimFetch('/api/command', { cmd });
out.value += data.output || '(no output)';
} catch (e) {
out.value += 'Error: ' + e.message;
}
pysimRefresh();
}
async function pysimApdu() {
const apdu = document.getElementById('pysim-apdu').value.replace(/[^0-9a-fA-F]/g, '').toUpperCase();
if (!apdu) return;
const out = document.getElementById('pysim-apdu-output');
out.value = '> ' + apdu + '\n';
try {
const data = await pysimFetch('/api/apdu', { apdu });
if (data.error) {
out.value += 'Error: ' + data.error;
} else {
out.value += 'Response: ' + data.response + '\nSW: ' + data.sw;
}
} catch (e) {
out.value += 'Error: ' + e.message;
}
pysimRefresh();
}
function spParams() {
return {
spi1: document.getElementById('sp-spi1').value,
spi2: document.getElementById('sp-spi2-hex').value,
kic: document.getElementById('sp-kic-hex').value,
kid: document.getElementById('sp-kid-hex').value,
tar: document.getElementById('sp-tar').value,
cntr: document.getElementById('sp-cntr').value,
kicKey: document.getElementById('sp-kic-key').value,
kidKey: document.getElementById('sp-kid-key').value,
apdu: document.getElementById('sp-apdu').value,
};
}
async function pysimVerifySp() {
const sp = document.getElementById('sp-result').value.replace(/[^0-9a-fA-F]/g, '').toUpperCase();
const resultEl = document.getElementById('sp-verify-result');
if (!sp) { alert('No secured packet to verify. Generate a secure packet first.'); return; }
resultEl.classList.remove('hidden');
resultEl.textContent = t('Verifying against pySim reference...');
try {
const data = await pysimFetch('/api/sp-verify', { sp, ...spParams() });
if (data.match) {
resultEl.textContent = 'MATCH: JS packet byte-identical to pySim reference (' + sp.length / 2 + ' bytes)';
resultEl.classList.remove('text-red-600');
resultEl.classList.add('text-green-600');
} else {
let msg = 'MISMATCH vs pySim.';
if (data.diffs) msg += ' First diff at byte ' + data.diffs[0].offset + ' (JS=' + data.diffs[0].js + ' pySim=' + data.diffs[0].ref + ')';
if (data.py_sp) msg += ' pySim reference: ' + data.py_sp;
resultEl.textContent = msg;
resultEl.classList.remove('text-green-600');
resultEl.classList.add('text-red-600');
}
} catch (e) {
resultEl.textContent = 'Error: ' + e.message;
resultEl.classList.remove('text-green-600');
resultEl.classList.add('text-red-600');
}
}
async function pysimSendOta() {
const connectedRow = document.getElementById('pysim-connected-row');
if (!connectedRow || connectedRow.classList.contains('hidden')) {
alert('Card reader server is not connected');
return;
}
const sp = document.getElementById('sp-result').value.replace(/[^0-9a-fA-F]/g, '').toUpperCase();
if (!sp) { alert('No secured packet to send. Generate a secure packet first.'); return; }
const sendResultEl = document.getElementById('sp-send-result');
const porStatusEl = document.getElementById('sp-por-status');
sendResultEl.classList.remove('hidden', 'text-red-600', 'text-green-600');
sendResultEl.classList.add('text-gray-500');
sendResultEl.textContent = t('Sending OTA...');
porStatusEl.classList.add('hidden');
try {
const data = await pysimFetch('/api/send-ota', { sp, ...spParams() });
if (data.success) {
if (data.por && data.por.decoded) {
document.getElementById('resp-sw').value = data.por.decoded.last_status_word || '';
document.getElementById('resp-data').value = data.por.decoded.last_response_data || '';
switchTab('response');
}
let msg = 'OTA sent. SW: ' + data.sw;
const por = data.por;
if (por && por.response_status) {
msg += ' | PoR status: ' + por.response_status + ' (TAR ' + por.tar + ')';
if (por.cntr) msg += ' | PoR CNTR: ' + por.cntr;
if (por.decoded && por.decoded.last_status_word) msg += ' | last SW ' + por.decoded.last_status_word + ' ' + (por.decoded.last_response_data || '');
const okPor = por.response_status === 'por_ok';
porStatusEl.textContent = 'PoR: ' + por.response_status;
porStatusEl.classList.remove('hidden', okPor ? 'text-red-600' : 'text-green-600');
porStatusEl.classList.add(okPor ? 'text-green-600' : 'text-red-600');
if (okPor) {
// packet was accepted and executed - advance the counter so the
// same secured packet can never be sent twice (v1.9.6)
const cntrEl = document.getElementById('sp-cntr');
cntrEl.value = ((parseInt(cntrEl.value, 16) || 0) + 1)
.toString(16).toUpperCase().padStart(10, '0').slice(-10);
document.getElementById('sp-result').value = '';
// keep the selected card preset in sync with the new counter (v1.9.8)
const selIdx = parseInt(document.getElementById('sp-card-sel').value, 10);
if (!isNaN(selIdx) && cards[selIdx]) {
cards[selIdx].cntr = cntrEl.value;
cardsSave();
cardsRender();
}
}
}
sendResultEl.textContent = msg;
if (por && por.raw) {
const rawLine = document.createElement('div');
rawLine.className = 'break-all';
rawLine.textContent = 'raw PoR: ' + por.raw;
sendResultEl.appendChild(rawLine);
}
sendResultEl.classList.remove('hidden', 'text-red-600', 'text-gray-500');
sendResultEl.classList.add('text-green-600');
} else {
let msg = 'OTA failed';
if (data.error) msg += ': ' + data.error;
if (data.sw) msg += (data.error ? ' | ' : ': ') + 'SW ' + data.sw;
sendResultEl.textContent = msg;
sendResultEl.classList.remove('hidden', 'text-green-600', 'text-gray-500');
sendResultEl.classList.add('text-red-600');
}
} catch (e) {
sendResultEl.textContent = 'Error: ' + e.message;
sendResultEl.classList.remove('hidden', 'text-green-600', 'text-gray-500');
sendResultEl.classList.add('text-red-600');
}
}
// ===== STK Menu Browser =====
let stkMenuStack = [];
async function stkCheckMenu() {
try {
console.log('stkCheckMenu called');
const data = await pysimFetch('/api/menu');
console.log('menu data:', data);
const btn = document.getElementById('stk-menu-btn');
if (data && data.title && data.items && data.items.length) {
document.getElementById('stk-menu-title').textContent = data.title;
btn.style.display = '';
} else {
btn.style.display = 'none';
}
} catch (e) {
console.error('stkCheckMenu error:', e);
}
}
function stkSetPillsDisabled(disabled) {
document.querySelectorAll('.pysim-subtab').forEach(b => {
if (b.id === 'stk-menu-btn') return;
b.disabled = disabled;
b.classList.toggle('opacity-50', disabled);
});
}
function stkMenuOpen() {
document.getElementById('stk-menu-panel-title').textContent = document.getElementById('stk-menu-title').textContent;
stkSetPillsDisabled(true);
stkMenuStack = [];
document.getElementById('stk-back-btn').style.display = 'none';
document.getElementById('stk-menu-buttons').classList.add('hidden');
document.getElementById('stk-menu-panel').classList.remove('hidden');
stkMenuRenderItems();
}
function stkMenuClose() {
document.getElementById('stk-menu-panel').classList.add('hidden');
stkSetPillsDisabled(false);
}
async function stkMenuExit() {
try {
await pysimFetch('/api/menu-respond', { result: 'cancel' });
} catch (e) {}
stkMenuClose();
}
function stkMenuRenderItems() {
const content = document.getElementById('stk-menu-panel-content');
content.innerHTML = '<span class="text-gray-500 text-sm">Loading...</span>';
pysimFetch('/api/menu').then(data => {
if (!data || !data.items) { content.innerHTML = '<span class="text-red-500 text-sm">No menu data</span>'; return; }
let html = '<div class="space-y-1">';
data.items.forEach(item => {
html += '<div class="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-gray-100 dark:hover:bg-slate-700 cursor-pointer" onclick="stkMenuItemClick(' + item.id + ')"><span class="w-5 text-xs text-gray-300 dark:text-gray-600 text-right">' + item.id + '</span><span class="text-sm">' + esc(item.text) + '</span></div>';
});
html += '</div>';
content.innerHTML = html;
}).catch(() => { content.innerHTML = '<span class="text-red-500 text-sm">Error loading menu</span>'; });
}
async function stkMenuItemClick(itemId) {
stkSetPillsDisabled(true);
const content = document.getElementById('stk-menu-panel-content');
content.innerHTML = '<span class="text-gray-500 text-sm">Sending...</span>';
try {
const data = await pysimFetch('/api/menu-select', { item_id: itemId });
stkMenuHandleResponse(data);
} catch (e) {
content.innerHTML = '<span class="text-red-500 text-sm">Error: ' + esc(e.message) + '</span>';
stkSetPillsDisabled(false);
}
}
function stkMenuHandleResponse(data) {
const content = document.getElementById('stk-menu-panel-content');
const backBtn = document.getElementById('stk-back-btn');
const btns = document.getElementById('stk-menu-buttons');
btns.classList.add('hidden');
if (data.type === 'display_text') {
content.innerHTML = '<div class="text-sm whitespace-pre-wrap p-2 bg-gray-50 dark:bg-slate-700 rounded">' + esc(data.text) + '</div>';
backBtn.style.display = '';
document.getElementById('stk-ok-btn').style.display = '';
btns.classList.remove('hidden');
} else if (data.type === 'select_item') {
stkMenuStack.push(data.items);
content.innerHTML = '';
const items = data.items;
let html = '<div class="space-y-1">';
items.forEach(item => {
html += '<div class="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-gray-100 dark:hover:bg-slate-700 cursor-pointer" onclick="stkSubItemClick(' + item.id + ')"><span class="w-5 text-xs text-gray-300 dark:text-gray-600 text-right">' + item.id + '</span><span class="text-sm">' + esc(item.text) + '</span></div>';
});
html += '</div>';
content.innerHTML = html;
backBtn.style.display = '';
document.getElementById('stk-ok-btn').style.display = 'none';
btns.classList.remove('hidden');
} else {
content.innerHTML = '<span class="text-gray-500 text-sm">SW: ' + esc(data.sw || '?') + '</span>';
stkSetPillsDisabled(false);
backBtn.style.display = 'none';
}
}
async function stkSubItemClick(itemId) {
const content = document.getElementById('stk-menu-panel-content');
content.innerHTML = '<span class="text-gray-500 text-sm">Sending...</span>';
try {
const data = await pysimFetch('/api/menu-respond', { result: 'ok', item_id: itemId });
stkMenuHandleResponse(data);
} catch (e) {
content.innerHTML = '<span class="text-red-500 text-sm">Error: ' + esc(e.message) + '</span>';
}
}
async function stkMenuRespond(result) {
try {
const data = await pysimFetch('/api/menu-respond', { result: result });
if (result === 'cancel' || result === 'back' || result === 'timeout') {
stkMenuRenderItems();
document.getElementById('stk-menu-buttons').classList.add('hidden');
document.getElementById('stk-back-btn').style.display = 'none';
return;
}
stkMenuHandleResponse(data);
} catch (e) {
document.getElementById('stk-menu-panel-content').innerHTML = '<span class="text-red-500 text-sm">Error: ' + esc(e.message) + '</span>';
}
}
// ===== Cards storage =====
let cards = [];
function cardsLoad() {
try {
const d = localStorage.getItem('otaman_cards');
cards = d ? JSON.parse(d) : [];
} catch (e) { cards = []; }
cardsRebuildSelect();
}
function cardsSave() {
localStorage.setItem('otaman_cards', JSON.stringify(cards));
cardsRebuildSelect();
cardsRender();
}
function cardsAdd() {
const name = document.getElementById('cards-name').value.trim();
const iccid = document.getElementById('cards-iccid').value.trim();
const kic = document.getElementById('cards-kic').value.trim();
const kid = document.getElementById('cards-kid').value.trim();
const spi1 = document.getElementById('cards-spi1').value.trim();
const spi2 = document.getElementById('cards-spi2').value.trim();
const tar = document.getElementById('cards-tar').value.trim();
const cntr = document.getElementById('cards-cntr').value.trim();
const kicKey = document.getElementById('cards-kic-key').value.trim();
const kidKey = document.getElementById('cards-kid-key').value.trim();
if (!name) { alert('Name is required'); return; }
if (!kic || !kid) { alert('KIc and KID are required'); return; }
if (!kicKey || !kidKey) { alert('KIc key and KID key are required'); return; }
if (iccid && cards.some(c => c.iccid === iccid)) { alert('Card with this ICCID already exists'); return; }
cards.push({
name: name,
iccid: iccid,
kic: kic,
kid: kid,
spi1: spi1 || '16',
spi2: spi2 || '01',
tar: tar,
cntr: cntr || '0000000001',
kicKey: kicKey,
kidKey: kidKey,
});
cardsSave();
['name','iccid','kic','kid','spi1','spi2','tar','cntr','kic-key','kid-key'].forEach(id => {
document.getElementById('cards-' + id).value = '';
});
}
function cardsRemove(i) {
cards.splice(i, 1);
cardsSave();
cardsRender();
}
function cardsRender() {
const tbody = document.getElementById('cards-tbody');
if (!cards.length) { tbody.innerHTML = '<tr><td colspan="9" class="py-2 text-gray-400">' + t('No cards defined.') + '</td></tr>'; return; }
let html = '';
for (let i = 0; i < cards.length; i++) {
const c = cards[i];
html += '<tr class="border-b border-gray-100 dark:border-slate-700">';
html += '<td class="py-1 px-2 font-medium">' + esc(c.name) + '</td>';
html += '<td class="py-1 px-2 font-mono text-xs">' + esc(c.iccid) + '</td>';
html += '<td class="py-1 px-2 font-mono text-xs text-center">' + esc(c.kic) + '</td>';
html += '<td class="py-1 px-2 font-mono text-xs text-center">' + esc(c.kid) + '</td>';
html += '<td class="py-1 px-2 font-mono text-xs text-center">' + esc(c.spi1) + '</td>';
html += '<td class="py-1 px-2 font-mono text-xs text-center">' + esc(c.spi2) + '</td>';
html += '<td class="py-1 px-2 font-mono text-xs">' + esc(c.tar || '') + '</td>';
html += '<td class="py-1 px-2 font-mono text-xs">' + esc(c.cntr) + '</td>';
html += '<td class="py-1 px-2"><button onclick="cardsRemove(' + i + ')" class="px-2 py-0.5 text-xs rounded bg-red-100 text-red-700 hover:bg-red-200">' + t('Remove') + '</button></td>';
html += '</tr>';
}
tbody.innerHTML = html;
}
function cardsRebuildSelect() {
const sel = document.getElementById('sp-card-sel');
if (!sel) return;
let html = '<option value="">' + t('— Select card —') + '</option>';
for (let i = 0; i < cards.length; i++) {
html += '<option value="' + i + '">' + esc(cards[i].name) + ' (' + esc(cards[i].iccid) + ')</option>';
}
sel.innerHTML = html;
}
function cardsApply(idx) {
const c = cards[parseInt(idx)];
if (!c) return;
document.getElementById('sp-spi1').value = c.spi1;
document.getElementById('sp-spi2-hex').value = c.spi2;
document.getElementById('sp-kic-hex').value = c.kic;
document.getElementById('sp-kid-hex').value = c.kid;
document.getElementById('sp-cntr').value = c.cntr;
document.getElementById('sp-kic-key').value = c.kicKey;
document.getElementById('sp-kid-key').value = c.kidKey;
spInvalidate();
updateSp();
genSp();
}
function cardsExport() {
const el = document.getElementById('cards-io');
el.value = JSON.stringify(cards, null, 2);
el.classList.remove('hidden');
el.select();
}
function downloadJson(name, obj) {
const blob = new Blob([JSON.stringify(obj, null, 2)], {type: 'application/json'});
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = name;
a.click();
URL.revokeObjectURL(a.href);
}
function cardsExportFile() {
downloadJson('otaman-cards.json', cards);
}
async function cardsImport(text) {
try {
if (text == null) text = await navigator.clipboard.readText();
const imported = JSON.parse(text);
if (!Array.isArray(imported)) throw new Error('Expected an array');
let added = 0;
for (const entry of imported) {
if (!entry.name) continue;
if (entry.iccid && cards.some(c => c.iccid === entry.iccid)) continue;
cards.push({
name: entry.name,
iccid: entry.iccid,
kic: entry.kic || '15',
kid: entry.kid || '15',
spi1: entry.spi1 || '16',
spi2: entry.spi2 || '01',
tar: entry.tar || '',
cntr: entry.cntr || '0000000001',
kicKey: entry.kicKey || '',
kidKey: entry.kidKey || '',
});
added++;
}
cardsSave();
cardsRender();
const el = document.getElementById('cards-io');
el.value = 'Imported ' + added + ' cards.';
setTimeout(() => { el.classList.add('hidden'); }, 2000);
} catch (e) {
alert('Import error: ' + e.message);
}
}
function cardsImportFile(input) {
const file = input.files && input.files[0];
input.value = '';
if (!file) return;
const reader = new FileReader();
reader.onload = () => cardsImport(reader.result);
reader.readAsText(file);
}
function cardsImportField() {
const el = document.getElementById('cards-io');
const val = el.value.trim();
if (!el.classList.contains('hidden') && val.startsWith('[')) { cardsImport(val); return; }
el.value = '';
el.placeholder = 'Paste JSON array here, then press Paste & import again';
el.classList.remove('hidden');
el.focus();
}
// ===== pysim sub-tabs =====
let pysimCmdHistory = [];
let pysimCmdHistIdx = -1;
let _pysimAutoConnectAttempted = false;
function pysimSwitchSubtab(name) {
document.querySelectorAll('.pysim-subtab').forEach(btn => {
const isActive = btn.dataset.pysimSub === name;
btn.classList.toggle('bg-blue-600', isActive);
btn.classList.toggle('text-white', isActive);
btn.classList.toggle('bg-gray-200', !isActive);
btn.classList.toggle('dark:bg-slate-700', !isActive);
btn.classList.toggle('text-gray-700', !isActive);
btn.classList.toggle('dark:text-slate-300', !isActive);
});
['cmd', 'apdu', 'files', 'custom', 'proactive'].forEach(s => {
document.getElementById('pysim-sub-' + s).classList.toggle('hidden', s !== name);
});
if (name === 'custom') pysimCustomRender();
if (name === 'proactive') { pysimEventsRender(); pysimProactiveLogRender(); pysimPliRender(); pysimPollStatusInit(); }
pysimHelpAnchor = {cmd:'pysim-cmdline', apdu:'raw-apdu', files:'file-manager', custom:'custom-files', proactive:'proactive-uicc'}[name] || 'card-reader';
setHelpAnchor(pysimHelpAnchor);
}
function pysimCmdKeydown(e) {
if (e.key === 'Enter') {
const cmd = e.target.value.trim();
if (cmd) {
pysimCmdHistory.push(cmd);
pysimCmdHistIdx = pysimCmdHistory.length;
}
pysimExec();
document.getElementById('pysim-cmd-hint').classList.add('hidden');
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (pysimCmdHistIdx > 0) {
pysimCmdHistIdx--;
e.target.value = pysimCmdHistory[pysimCmdHistIdx];
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
if (pysimCmdHistIdx < pysimCmdHistory.length - 1) {
pysimCmdHistIdx++;
e.target.value = pysimCmdHistory[pysimCmdHistIdx];
} else {
pysimCmdHistIdx = pysimCmdHistory.length;
e.target.value = '';
}
}
}
let pysimCmdHintTimeout = null;
async function pysimCmdHint() {
clearTimeout(pysimCmdHintTimeout);
pysimCmdHintTimeout = setTimeout(async () => {
const input = document.getElementById('pysim-cmd');
const text = input.value.trim();
const cmd = text.split(/\s+/)[0];
if (!cmd || text.includes(' ')) {
document.getElementById('pysim-cmd-hint').classList.add('hidden');
return;
}
const hintEl = document.getElementById('pysim-cmd-hint');
try {
const data = await pysimFetch('/api/help', { cmd });
if (data.usage) {
hintEl.textContent = data.usage;
hintEl.classList.remove('hidden');
} else {
hintEl.classList.add('hidden');
}
} catch (e) {
hintEl.classList.add('hidden');
}
}, 300);
}
let pysimCmdList = [];
async function pysimLoadCommands() {
try {
const data = await pysimFetch('/api/commands');
if (Array.isArray(data)) pysimCmdList = data;
} catch (e) {}
}
function pysimCmdInput() {
const input = document.getElementById('pysim-cmd');
const text = input.value;
const suggestEl = document.getElementById('pysim-cmd-suggest');
const hintEl = document.getElementById('pysim-cmd-hint');
if (text.includes(' ')) {
suggestEl.classList.add('hidden');
const cmd = text.split(/\s+/)[0];
if (cmd) {
clearTimeout(pysimCmdHintTimeout);
pysimCmdHintTimeout = setTimeout(async () => {
try {
const data = await pysimFetch('/api/help', { cmd });
if (data.usage) {
hintEl.textContent = data.usage;
hintEl.classList.remove('hidden');
} else {
hintEl.classList.add('hidden');
}
} catch (e) {
hintEl.classList.add('hidden');
}
}, 300);
return;
}
hintEl.classList.add('hidden');
return;
}
hintEl.classList.add('hidden');
if (!text) { suggestEl.classList.add('hidden'); return; }
const matches = pysimCmdList.filter(c => c.startsWith(text)).slice(0, 20);
if (matches.length === 0) { suggestEl.classList.add('hidden'); return; }
suggestEl.innerHTML = matches.map(c =>
'<div class="px-2 py-1 cursor-pointer hover:bg-gray-100 dark:hover:bg-slate-700" onclick="pysimCmdSelect(\'' + esc(c) + '\')">' + esc(c) + '</div>'
).join('');
suggestEl.classList.remove('hidden');
}
function pysimCmdSelect(cmd) {
document.getElementById('pysim-cmd').value = cmd;
document.getElementById('pysim-cmd-suggest').classList.add('hidden');
document.getElementById('pysim-cmd').focus();
}
// ===== pysim file browser =====
let pysimFsTreeRoot = null;
function getParentSel(node) {
if (!node || !node.parent) return null;
const p = node.parent;
if (p.name === 'MF') return 'MF';
if (p.name.startsWith('ADF.')) return p.name;
if (p.fid) return p.fid;
return p.name;
}
function pysimCustomInject(node) {
if (!node || !node.fid) return;
const pfid = node.fid.toLowerCase();
for (const c of pysimCustomFiles) {
if (c.parentFid.toLowerCase() !== pfid) continue;
const existing = (node.children || []).find(ch => ch.fid?.toLowerCase() === c.fid.toLowerCase());
if (existing) {
existing.name = c.name;
existing.custom = true;
} else {
node.children = node.children || [];
node.children.push({
name: c.name,
fid: c.fid,
isDir: c.name.startsWith('DF.') || c.name.startsWith('ADF.'),
children: null,
expanded: false,
exists: null,
custom: true,
parent: node,
});
}
}
}
async function pysimFsRefresh() {
const detailEl = document.getElementById('pysim-fs-detail');
detailEl.classList.add('hidden');
const data = await pysimFetch('/api/tree', { name: 'MF' });
pysimFsTreeRoot = {
name: data.name || 'MF',
path: data.name || 'MF',
fid: data.fid || '3F00',
isDir: true,
children: null,
expanded: true,
parent: null,
};
pysimFsTreeRoot.children = (data.children || []).map(c => ({
name: c.name,
fid: c.fid,
isDir: c.isDir,
aid: c.aid,
children: null,
expanded: false,
exists: true,
parent: pysimFsTreeRoot,
}));
pysimFsTreeRoot.exists = true;
pysimCustomInject(pysimFsTreeRoot);
pysimFsRenderTree();
}
async function pysimFsLoadChildren(node) {
if (node.children) return;
try {
let body = { name: node.name, fid: node.fid };
const parentSel = getParentSel(node);
if (parentSel) body.parent_sel = parentSel;
let data = await pysimFetch('/api/tree', body);
if (!data || data.exists === false) {
body = { name: node.name, fid: node.fid };
data = await pysimFetch('/api/tree', body);
}
if (!data || data.exists === false) {
throw new Error('File not found');
}
node.children = (data.children || []).map(c => ({
name: c.name,
fid: c.fid,
isDir: c.isDir,
aid: c.aid,
children: null,
expanded: false,
exists: true,
parent: node,
}));
node.exists = true;
pysimCustomInject(node);
} catch (e) {
node.exists = false;
node.children = [];
pysimFsRenderTree();
}
}
function pysimFsRenderTree() {
const el = document.getElementById('pysim-fs-tree');
if (!pysimFsTreeRoot) { el.innerHTML = '(loading)'; return; }
el.innerHTML = pysimFsRenderNode(pysimFsTreeRoot, 0);
}
function pysimFsRenderNode(node, depth) {
if (!node) return '';
const pad = '&nbsp;'.repeat(depth * 4);
const isDir = node.isDir;
const exists = node.exists !== false;
const verified = node.exists !== null;
const color = verified ? (exists ? (isDir ? 'text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-slate-300') : 'text-red-400 dark:text-red-400') : 'text-gray-500 dark:text-slate-400';
const italic = node.custom ? ' italic' : '';
const fidStr = node.fid ? ' <span class="text-gray-400 dark:text-slate-500">(' + node.fid.toUpperCase() + ')</span>' : '';
const aidStr = node.aid ? ' <span class="text-gray-400 dark:text-slate-500">[' + node.aid + ']</span>' : '';
let html = '<div class="' + color + italic + '">';
html += pad;
if (verified && !exists) {
html += '✗ ';
}
if (isDir) {
if ((verified && exists) || !verified) {
const toggle = node.children && node.expanded ? '▼' : '▶';
html += '<span class="cursor-pointer select-none' + italic + '" onclick="pysimFsToggleDir(\'' + esc(node.name) + '\')">' + toggle + '</span> ';
html += '<span class="cursor-pointer hover:underline' + italic + '" onclick="pysimFsToggleDir(\'' + esc(node.name) + '\')">' + esc(node.name) + fidStr + aidStr + '</span>';
} else {
html += esc(node.name) + fidStr + aidStr + '</div>';
return html;
}
} else {
html += '<span class="cursor-pointer hover:underline ml-4' + italic + '"' +
((verified && exists) ? ' onclick="pysimFsClickFile(\'' + esc(node.name) + '\')"' : (!verified ? ' onclick="pysimFsClickFile(\'' + esc(node.name) + '\')"' : '')) + '>' + esc(node.name) + fidStr + aidStr + '</span>';
}
html += '</div>';
if (isDir && node.children && node.expanded) {
for (const child of node.children) {
html += pysimFsRenderNode(child, depth + 1);
}
}
return html;
}
function pysimFsFindNode(name, root) {
if (!root) return null;
if (root.name === name) return root;
if (root.children) {
for (const c of root.children) {
const found = pysimFsFindNode(name, c);
if (found) return found;
}
}
return null;
}
async function pysimFsToggleDir(name) {
const node = pysimFsFindNode(name, pysimFsTreeRoot);
if (!node || !node.isDir) return;
if (!node.children) {
await pysimFsLoadChildren(node);
}
node.expanded = !node.expanded;
pysimFsRenderTree();
if (node.expanded) pysimRefresh();
}
async function pysimFsClickFile(name) {
pysimFsResetEdit();
pysimFsSetMode('raw');
const node = pysimFsFindNode(name, pysimFsTreeRoot);
let body = { name: name, fid: node ? node.fid : null };
const parentSel = getParentSel(node);
if (parentSel) body.parent_sel = parentSel;
let sel = await pysimFetch('/api/select', body);
if (!sel || sel.exists === false) {
body = { name: name, fid: node ? node.fid : null };
sel = await pysimFetch('/api/select', body);
}
const exists = sel && sel.exists !== false;
if (node) {
node.exists = exists;
if (!exists) node.children = [];
pysimFsRenderTree();
}
const ft = exists ? sel.file_type : null;
const label = ({ transparent: 'TR', linear_fixed: 'LF', cyclic: 'CY', ber_tlv: 'BT', df: 'DF' })[ft] || '';
const prefix = exists ? '' : '✗ ';
document.getElementById('pysim-fs-filename').textContent = prefix + name + (label ? ' [' + label + ']' : '');
document.getElementById('pysim-fs-content').innerHTML = '';
document.getElementById('pysim-fs-status').textContent = exists ? '' : '✗ File not found on card';
if (exists) {
document.getElementById('pysim-fs-detail').classList.remove('hidden');
} else {
document.getElementById('pysim-fs-detail').classList.add('hidden');
}
pysimRefresh();
}
async function pysimFsRead() {
const rawName = document.getElementById('pysim-fs-filename').textContent.replace(/ \[.*?\]$/, '').replace(/^✗ /, '');
const out = document.getElementById('pysim-fs-content');
const statusEl = document.getElementById('pysim-fs-status');
out.innerHTML = '';
statusEl.textContent = 'Loading...';
try {
const node = pysimFsFindNode(rawName, pysimFsTreeRoot);
let body = { name: rawName, fid: node ? node.fid : null, mode: pysimFsDecodedMode ? 'decoded' : 'raw' };
const parentSel = getParentSel(node);
if (parentSel) body.parent_sel = parentSel;
let data = await pysimFetch('/api/read', body);
if (!data.success) {
body = { name: rawName, fid: node ? node.fid : null, mode: pysimFsDecodedMode ? 'decoded' : 'raw' };
data = await pysimFetch('/api/read', body);
}
if (!data.success) {
statusEl.textContent = 'SW: ' + (data.sw || '?') + ' — ' + (data.error || 'Error');
return;
}
statusEl.textContent = 'SW: ' + data.sw + ' OK';
if (data.decoded) {
out.innerHTML = '<pre class="text-xs leading-relaxed">' + esc(JSON.stringify(data.decoded, null, 2)) + '</pre>';
} else if (data.records) {
let html = '';
for (const r of data.records) {
html += '<div class="flex items-center gap-1 mb-1">' +
'<input type="checkbox" class="pysim-fs-record-cb shrink-0 hidden">' +
'<span class="text-xs w-8 text-gray-500 dark:text-slate-400 shrink-0">' + r.num + '</span>' +
'<input class="flex-1 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 bg-white dark:bg-slate-800 text-gray-800 dark:text-slate-200" style="overflow-x:auto;min-width:100px" value="' + esc(r.data) + '" readonly>' +
'</div>';
}
out.innerHTML = html || '(no records)';
} else {
out.innerHTML = '<textarea rows="8" readonly class="w-full font-mono text-xs border-0 bg-transparent resize-none">' + esc(data.data || '(no output)') + '</textarea>';
}
} catch (e) {
statusEl.textContent = 'Error: ' + e.message;
}
}
async function pysimFsSelect() {
const name = document.getElementById('pysim-fs-filename').textContent;
const node = pysimFsFindNode(name, pysimFsTreeRoot);
if (node && node.fid) {
await pysimFetch('/api/command', { cmd: 'select ' + node.fid });
} else {
await pysimFetch('/api/command', { cmd: 'select ' + name });
}
pysimFsRefresh();
pysimRefresh();
}
let pysimFsEditMode = false;
let pysimFsEditData = null;
function pysimFsEdit() {
pysimFsEditMode = true;
document.getElementById('pysim-fs-read-btn').classList.add('hidden');
document.getElementById('pysim-fs-edit-btn').classList.add('hidden');
document.getElementById('pysim-fs-save-btn').classList.remove('hidden');
document.getElementById('pysim-fs-cancel-btn').classList.remove('hidden');
const out = document.getElementById('pysim-fs-content');
out.querySelectorAll('textarea, input:not([type=checkbox])').forEach(el => el.removeAttribute('readonly'));
out.querySelectorAll('.pysim-fs-record-cb').forEach(cb => cb.classList.remove('hidden'));
out.querySelectorAll('input:not([type=checkbox])').forEach(el => {
el.addEventListener('input', function() {
const cb = this.closest('div')?.querySelector('.pysim-fs-record-cb');
if (cb) cb.checked = true;
});
});
pysimFsEditData = out.innerHTML;
}
function pysimFsCancel() {
pysimFsEditMode = false;
document.getElementById('pysim-fs-read-btn').classList.remove('hidden');
document.getElementById('pysim-fs-edit-btn').classList.remove('hidden');
document.getElementById('pysim-fs-save-btn').classList.add('hidden');
document.getElementById('pysim-fs-cancel-btn').classList.add('hidden');
const out = document.getElementById('pysim-fs-content');
if (pysimFsEditData) out.innerHTML = pysimFsEditData;
pysimFsEditData = null;
}
async function pysimFsSave() {
const rawName = document.getElementById('pysim-fs-filename').textContent.replace(/ \[.*?\]$/, '').replace(/^✗ /, '');
const statusEl = document.getElementById('pysim-fs-status');
const out = document.getElementById('pysim-fs-content');
const node = pysimFsFindNode(rawName, pysimFsTreeRoot);
statusEl.textContent = 'Saving...';
try {
const textareas = out.querySelectorAll('textarea');
if (textareas.length === 1) {
// Transparent file
const body = { name: rawName, fid: node ? node.fid : null, data: textareas[0].value.replace(/[^0-9a-fA-F]/g, '') };
const parentSel = getParentSel(node);
if (parentSel) body.parent_sel = parentSel;
const data = await pysimFetch('/api/write', body);
if (data.success) {
statusEl.textContent = 'SW: ' + data.sw + ' OK';
pysimFsCancel();
pysimFsRead();
} else {
statusEl.textContent = 'SW: ' + (data.sw || '?') + ' — ' + (data.error || 'Error');
}
} else {
// Record file
const inputs = out.querySelectorAll('input:not([type=checkbox])');
let wroteAny = false;
for (const inp of inputs) {
const row = inp.closest('div');
const cb = row?.querySelector('.pysim-fs-record-cb');
if (cb && !cb.checked) continue;
wroteAny = true;
const num = row?.querySelector('span')?.textContent;
if (num) {
const body = { name: rawName, fid: node ? node.fid : null, data: inp.value.replace(/[^0-9a-fA-F]/g, ''), record_nr: parseInt(num) };
const parentSel = getParentSel(node);
if (parentSel) body.parent_sel = parentSel;
const data = await pysimFetch('/api/write', body);
if (!data.success) {
statusEl.textContent = 'SW: ' + (data.sw || '?') + ' — ' + (data.error || 'Error') + ' (record ' + num + ')';
return;
}
}
}
if (!wroteAny) {
statusEl.textContent = 'No records selected — check the ones you want to update';
return;
}
statusEl.textContent = 'SW: 9000 OK';
pysimFsCancel();
pysimFsRead();
}
} catch (e) {
statusEl.textContent = 'Error: ' + e.message;
}
}
function pysimFsSetMode(mode) {
pysimFsDecodedMode = mode === 'dec';
document.querySelectorAll('.pysim-mode-pill').forEach(el => {
const active = el.dataset.mode === mode;
el.classList.toggle('bg-blue-600', active);
el.classList.toggle('text-white', active);
el.classList.toggle('bg-gray-200', !active);
el.classList.toggle('dark:bg-slate-700', !active);
el.classList.toggle('text-gray-700', !active);
el.classList.toggle('dark:text-slate-300', !active);
});
}
function pysimFsResetEdit() {
pysimFsEditMode = false;
pysimFsEditData = null;
document.getElementById('pysim-fs-read-btn').classList.remove('hidden');
document.getElementById('pysim-fs-edit-btn').classList.remove('hidden');
document.getElementById('pysim-fs-save-btn').classList.add('hidden');
document.getElementById('pysim-fs-cancel-btn').classList.add('hidden');
}
// ===== proactive UICC events =====
const EVENT_NAMES = {
0x00: 'MT call', 0x01: 'Call connected', 0x02: 'Call disconnected',
0x03: 'Location status', 0x04: 'User activity', 0x05: 'Idle screen available',
0x06: 'Card reader status', 0x07: 'Language selection',
0x08: 'Browser termination', 0x09: 'Data available',
0x0A: 'Channel status', 0x0B: 'Access Technology Change',
0x0C: 'Display parameters changed', 0x0D: 'Local connection',
0x0E: 'Network Search Mode Change', 0x0F: 'Browsing status',
0x10: 'Frames Information Change', 0x11: 'I-WLAN Access Status',
0x12: 'Network Rejection', 0x13: 'HCI Connectivity',
0x14: 'Change of UICC Access', 0x15: 'CSG Cell Change',
0x16: 'Contactless state request', 0x17: 'Profile Container',
0x18: 'LTE D2D Discovery Monitoring', 0x19: 'LTE D2D Communication Monitoring',
0x1A: 'LTE D2D Announcement Response', 0x1B: 'LTE D2D Revocation',
0x1C: 'LTE D2D Application Port', 0x1D: 'LTE D2D Security Recovery',
0x1E: 'Off-net Emergency Call', 0x1F: 'ECall Over IMS',
0x20: 'EARFCN Update', 0x21: 'SCEF Channel Status',
};
const REJECTION_CAUSES = [
{v:2,l:'02 \u2014 IMSI unknown in HLR/HSS'},{v:3,l:'03 \u2014 Illegal UE / MS'},
{v:5,l:'05 \u2014 IMEI / PEI not accepted'},{v:6,l:'06 \u2014 Illegal ME'},
{v:7,l:'07 \u2014 Services not allowed'},{v:8,l:'08 \u2014 Services + non-services not allowed'},
{v:9,l:'09 \u2014 UE identity cannot be derived'},{v:10,l:'0A \u2014 Implicitly detached / de-registered'},
{v:11,l:'0B \u2014 PLMN not allowed'},{v:12,l:'0C \u2014 Location / Tracking Area not allowed'},
{v:13,l:'0D \u2014 Roaming not allowed in this area'},{v:14,l:'0E \u2014 Services not allowed in this PLMN'},
{v:15,l:'0F \u2014 No Suitable Cells'},{v:16,l:'10 \u2014 MSC temporarily not reachable'},
{v:17,l:'11 \u2014 Network failure'},
{v:18,l:'12 \u2014 CS domain not avail. (EMM) / MAC fail (GMM)'},
{v:19,l:'13 \u2014 ESM failure (EMM) / Synch failure (GMM)'},
{v:20,l:'14 \u2014 MAC failure (EMM) / Congestion (GMM)'},
{v:21,l:'15 \u2014 Synch failure (EMM)'},{v:22,l:'16 \u2014 Congestion / PLMN rejected perm. (LU)'},
{v:23,l:'17 \u2014 UE security capabilities mismatch'},{v:24,l:'18 \u2014 Security mode rejected, unspecified'},
{v:25,l:'19 \u2014 Not authorized for this CSG'},{v:26,l:'1A \u2014 Non-3GPP/5G auth unacceptable'},
{v:27,l:'1B \u2014 N1 mode not allowed (5GMM)'},{v:28,l:'1C \u2014 Restricted service area (5GMM)'},
{v:31,l:'1F \u2014 Redirection to EPC required (5GMM)'},
{v:32,l:'20 \u2014 Service option not supported (LU)'},
{v:33,l:'21 \u2014 Requested service option not subscribed (LU)'},
{v:34,l:'22 \u2014 Service option temporarily out of order (LU)'},
{v:35,l:'23 \u2014 Requested service option not authorized (EMM)'},
{v:38,l:'26 \u2014 Call cannot be identified (LU)'},
{v:39,l:'27 \u2014 CS service temporarily not available (EMM)'},
{v:40,l:'28 \u2014 No EPS bearer context activated (EMM)'},
{v:42,l:'2A \u2014 Severe network failure (EMM)'},{v:43,l:'2B \u2014 LADN not available (5GMM)'},
{v:48,l:'30 \u2014 Retry upon entry into new cell (LU)'},
{v:62,l:'3E \u2014 No network slices available (5GMM)'},
{v:65,l:'41 \u2014 Maximum PDU sessions reached (5GMM)'},
{v:67,l:'43 \u2014 Insufficient resources for slice/DNN (5GMM)'},
{v:69,l:'45 \u2014 Insufficient resources for slice (5GMM)'},
{v:71,l:'47 \u2014 ngKSI already in use (5GMM)'},
{v:72,l:'48 \u2014 Non-3GPP access to 5GCN not allowed (5GMM)'},
{v:73,l:'49 \u2014 Serving network not authorized (5GMM)'},
{v:74,l:'4A \u2014 Temporarily not authorized for SNPN (5GMM)'},
{v:75,l:'4B \u2014 Permanently not authorized for SNPN (5GMM)'},
{v:76,l:'4C \u2014 Not authorized for this CAG (5GMM)'},
{v:90,l:'5A \u2014 Payload was not forwarded (5GMM)'},
{v:91,l:'5B \u2014 DNN not supported/subscribed in slice (5GMM)'},
{v:92,l:'5C \u2014 Insufficient UP resources for PDU session (5GMM)'},
{v:95,l:'5F \u2014 Semantically incorrect message'},{v:96,l:'60 \u2014 Invalid mandatory information'},
{v:97,l:'61 \u2014 Message type non-existent/not implemented'},
{v:98,l:'62 \u2014 Message type not compatible with protocol state'},
{v:99,l:'63 \u2014 IE non-existent/not implemented'},
{v:100,l:'64 \u2014 Conditional IE error'},{v:101,l:'65 \u2014 Message not compatible with protocol state'},
{v:111,l:'6F \u2014 Protocol error, unspecified'},
];
const EVENT_FORMS = {
0x00: { title: 'MT call', note: 'not_yet' },
0x01: { title: 'Call connected', note: 'not_yet' },
0x02: { title: 'Call disconnected', note: 'not_yet' },
0x03: { title: 'Location status',
fields: [
{ id: 'status', label: 'Service Status', type: 'select',
opts: [{v:0,l:'Normal service'},{v:1,l:'Limited service'},{v:2,l:'No service'}] }
],
build(v) { return '9B01' + (v.status|0).toString(16).padStart(2,'0'); }
},
0x04: null,
0x05: null,
0x06: { title: 'Card reader status',
fields: [
{ id: 'status', label: 'Status', type: 'select',
opts: [{v:0,l:'Card reader removed'},{v:1,l:'Card reader inserted'}] }
],
build(v) { return '' + (v.status|0).toString(16).padStart(2,'0'); }
},
0x07: { title: 'Language selection',
fields: [
{ id: 'lang', label: 'Language code', type: 'text', placeholder: 'ISO 639, e.g. en, ru', pattern: '[a-zA-Z]{2,3}' }
],
build(v) { const b = new TextEncoder().encode(v.lang||'en'); return b.length.toString(16).padStart(2,'0') + bytesToHex(Array.from(b)); }
},
0x08: { title: 'Browser termination', note: 'not_yet' },
0x09: null,
0x0A: { title: 'Channel status', note: 'not_yet' },
0x0B: { title: 'Access Technology Change',
fields: [
{ id: 'tech', label: 'Access Technology', type: 'select',
opts: [{v:0,l:'GSM'},{v:1,l:'GSM Compact'},{v:3,l:'UTRAN'},{v:4,l:'GSM w/EGPRS'},{v:5,l:'UTRAN w/HSDPA'},{v:6,l:'UTRAN w/HSUPA'},{v:7,l:'UTRAN w/HSPA+'},{v:8,l:'E-UTRAN'},{v:9,l:'E-UTRAN (4G)'},{v:10,l:'5G NR'},{v:11,l:'5G NR (NSA)'},{v:12,l:'Satellite'}] }
],
build(v) { return 'BF01' + (v.tech|0).toString(16).padStart(2,'0'); }
},
0x0C: null,
0x0D: null,
0x0E: null,
0x0F: null,
0x10: null,
0x11: { title: 'I-WLAN Access Status', note: 'not_yet' },
0x12: { title: 'Network Rejection',
fields: [
{ id: 'reg_type', label: 'Registration type', type: 'select', onchange: 'updateNetRejectionFields()',
opts: [
{v:0x00,l:'Normal Location Updating',g:'lu'},{v:0x01,l:'Periodic Location Updating',g:'lu'},
{v:0x02,l:'IMSI Attach',g:'lu'},{v:0x03,l:'GPRS Attach',g:'gprs'},
{v:0x04,l:'Combined GPRS/IMSI Attach',g:'gprs'},{v:0x05,l:'RA Updating',g:'gprs'},
{v:0x06,l:'Combined RA/LA Updating',g:'gprs'},{v:0x07,l:'Combined RA/LA Updating w/ IMSI Attach',g:'gprs'},
{v:0x08,l:'Periodic RA Updating',g:'gprs'},{v:0x09,l:'EPS Attach (EMM)',g:'eps'},
{v:0x0A,l:'Combined EPS/IMSI Attach (EMM)',g:'eps'},{v:0x0B,l:'TA Updating (TAU)',g:'eps'},
{v:0x0C,l:'Combined TA/LA Updating (TAU)',g:'eps'},{v:0x0D,l:'Combined TA/LA Updating w/ IMSI Attach (TAU)',g:'eps'},
{v:0x0E,l:'Periodic TA Updating (TAU)',g:'eps'},{v:0x0F,l:'Initial Registration (5GS)',g:'eps'},
{v:0x10,l:'Mobility Registration Updating (5GS)',g:'eps'},{v:0x11,l:'Periodic Registration Updating (5GS)',g:'eps'},
]},
{ id: 'mcc', label: 'MCC', type: 'text', pattern: '\\d{3}', placeholder: 'e.g. 250', g: 'lu,gprs,eps' },
{ id: 'mnc', label: 'MNC', type: 'text', pattern: '\\d{2,3}', placeholder: 'e.g. 01', g: 'lu,gprs,eps' },
{ id: 'lac', label: 'LAC', type: 'text', pattern: '[0-9A-Fa-f]{4}', placeholder: 'hex, e.g. 00FF', g: 'lu,gprs' },
{ id: 'rac', label: 'RAC', type: 'text', pattern: '[0-9A-Fa-f]{2}', placeholder: 'hex, e.g. 01', g: 'gprs' },
{ id: 'tac', label: 'TAC', type: 'text', pattern: '[0-9A-Fa-f]{4,6}', placeholder: '4 (E-UTRAN) or 6 (NR) hex chars', g: 'eps' },
{ id: 'access_tech', label: 'Access Technology', type: 'select',
opts: [{v:0x00,l:'GSM'},{v:0x03,l:'UTRAN'},{v:0x08,l:'E-UTRAN'},{v:0x0A,l:'NG-RAN'},{v:0x0B,l:'Satellite NG-RAN'},{v:0x0C,l:'Satellite E-UTRAN'}], g: 'lu,gprs,eps' },
{ id: 'cause', label: 'Rejection cause code', type: 'select', opts: REJECTION_CAUSES, g: 'lu,gprs,eps' },
{ id: 'ext_cause', label: 'Extended EMM cause (optional)', type: 'number', min: '0', max: '255', placeholder: '0255', g: 'eps' },
],
build(v) {
const rt = parseInt(v.reg_type) || 0;
const g = rt <= 0x02 ? 'lu' : rt <= 0x08 ? 'gprs' : 'eps';
let d = 'F401' + rt.toString(16).padStart(2,'0');
d += 'BF01' + (parseInt(v.access_tech)|0).toString(16).padStart(2,'0');
d += 'F501' + (parseInt(v.cause) || 2).toString(16).padStart(2,'0');
const pm = encPlmn(v.mcc||'250',v.mnc||'01');
if (g === 'lu') {
d += '9305' + pm + ((v.lac||'0000').toUpperCase());
} else if (g === 'gprs') {
d += 'F306' + pm + ((v.lac||'0000').toUpperCase()) + ((v.rac||'01').toUpperCase());
} else {
const tc = (v.tac||'0001').toUpperCase();
d += '7D0' + (3 + tc.length/2).toString(16) + pm + tc;
if (v.ext_cause !== '' && v.ext_cause !== undefined) {
d += '5701' + (parseInt(v.ext_cause)|0).toString(16).padStart(2,'0');
}
}
return d.toUpperCase();
}
},
0x13: null,
0x14: { title: 'Change of UICC Access',
fields: [{ id: 'tech', label: 'Access Technology', type: 'select',
opts: [{v:0,l:'GSM'},{v:3,l:'UTRAN'},{v:8,l:'E-UTRAN'},{v:10,l:'NG-RAN'}] }],
build(v) { return 'BF01' + (v.tech|0).toString(16).padStart(2,'0'); }
},
0x15: { title: 'CSG Cell Change', note: 'not_yet' },
0x16: null,
0x17: null,
};
const PLI_QUALIFIERS = [
{code:'00',name:'Location Info (MCC, MNC, LAC/TAC, Cell ID)'},{code:'01',name:'IMEI'},
{code:'02',name:'Network Measurement results'},{code:'03',name:'Date, time and time zone'},
{code:'04',name:'Language setting'},{code:'05',name:'Timing Advance'},
{code:'06',name:'Access Technology (single)'},{code:'08',name:'IMEISV'},
{code:'09',name:'Search Mode'},{code:'0A',name:'Battery charge state'},
{code:'0C',name:'Current WSID'},{code:'0D',name:'Broadcast Network info'},
{code:'0E',name:'Multiple Access Technologies'},{code:'0F',name:'Location Info (multi-RAT)'},
{code:'10',name:'NMR (multi-RAT)'},{code:'11',name:'CSG ID list + HNB name'},
{code:'12',name:'H(e)NB IP address'},{code:'13',name:'H(e)NB surrounding macrocells'},
{code:'14',name:'Current WLAN identifier'},{code:'15',name:'Slices information'},
{code:'16',name:'CAG information list'},{code:'17',name:'Rejected slices information'},
];
async function pysimEventsRender() {
const el = document.getElementById('pysim-event-list');
el.innerHTML = '<span class="text-gray-400">' + t('Loading...') + '</span>';
try {
const events = await pysimFetch('/api/events');
if (!events || !events.length) {
el.innerHTML = '<span class="text-gray-400">' + t('No events configured.') + '</span>';
return;
}
let html = '';
events.forEach(e => {
const name = EVENT_NAMES[e] || ('Event 0x' + e.toString(16).padStart(2, '0').toUpperCase());
html += '<div class="flex items-center gap-2 py-0.5"><span class="w-6 text-right text-gray-400">'
+ e.toString(16).padStart(2, '0').toUpperCase()
+ '</span> ' + esc(name)
+ '<button onclick="pysimEventSendForm(' + e + ')" class="ml-auto px-2 py-1 text-sm rounded bg-blue-600 text-white hover:bg-blue-700">' + t('Send') + '</button></div>';
});
el.innerHTML = html;
} catch (e) {
el.innerHTML = '<span class="text-red-500">Error: ' + esc(e.message) + '</span>';
}
}
let _eventFormType = null;
function pysimEventSendForm(eventType) {
const modal = document.getElementById('event-send-modal');
const title = document.getElementById('event-send-title');
const body = document.getElementById('event-send-body');
const err = document.getElementById('event-send-error');
const result = document.getElementById('event-send-result');
const btn = document.getElementById('event-send-btn');
err.classList.add('hidden');
result.classList.add('hidden');
err.textContent = '';
result.textContent = '';
const name = EVENT_NAMES[eventType] || ('0x' + eventType.toString(16).padStart(2,'0'));
const cfg = EVENT_FORMS[eventType];
_eventFormType = eventType;
if (!cfg) {
title.textContent = name + ' (' + t('Send') + ')';
body.innerHTML = '<p class="text-sm text-gray-600 dark:text-slate-400">' + t('Send') + ' <span class="font-mono">' + esc(name) + '</span> ' + t('event notification to the card?') + '</p>';
btn.textContent = t('Send');
modal.classList.remove('hidden');
return;
}
if (cfg.note === 'not_yet') {
title.textContent = name;
body.innerHTML = '<p class="text-sm text-gray-500 dark:text-slate-400">' + t('This event type is not yet supported.') + '</p>';
btn.classList.add('hidden');
} else {
title.textContent = name + ' (' + t('Send') + ')';
let html = '';
cfg.fields.forEach(f => {
const rg = f.g ? f.g.split(',') : null;
html += '<div class="event-field" data-groups="' + (f.g||'') + '" data-id="' + f.id + '">';
html += '<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300">' + t(f.label) + '</label>';
if (f.type === 'select') {
html += '<select id="ev-f-' + f.id + '"' + (f.onchange ? ' onchange="' + f.onchange + '"' : '') + ' class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800">';
f.opts.forEach(o => html += '<option value="' + o.v + '"' + (o.g ? ' data-gr="' + o.g + '"' : '') + '>' + esc(o.l) + '</option>');
html += '</select>';
} else {
const attrs = [];
if (f.pattern) attrs.push('pattern="' + f.pattern + '"');
if (f.placeholder) attrs.push('placeholder="' + f.placeholder + '"');
if (f.min !== undefined) attrs.push('min="' + f.min + '" max="' + f.max + '"');
html += '<input id="ev-f-' + f.id + '" type="' + f.type + '" ' + attrs.join(' ') + ' class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono">';
}
html += '</div>';
});
body.innerHTML = html;
btn.classList.remove('hidden');
if (eventType === 0x12) updateNetRejectionFields();
}
modal.classList.remove('hidden');
}
function updateNetRejectionFields() {
const sel = document.getElementById('ev-f-reg_type');
if (!sel) return;
const opt = sel.options[sel.selectedIndex];
const g = opt ? opt.getAttribute('data-gr') || 'lu' : 'lu';
document.querySelectorAll('#event-send-body .event-field').forEach(div => {
const groups = div.getAttribute('data-groups') || '';
const show = groups === '' || groups.indexOf(g) >= 0;
div.style.display = show ? '' : 'none';
});
}
async function pysimEventSend() {
const err = document.getElementById('event-send-error');
const result = document.getElementById('event-send-result');
const btn = document.getElementById('event-send-btn');
err.classList.add('hidden');
result.classList.add('hidden');
const cfg = EVENT_FORMS[_eventFormType];
let eventData = null;
if (cfg && cfg.build) {
const values = {};
let valid = true;
cfg.fields.forEach(f => {
const el = document.getElementById('ev-f-' + f.id);
if (!el || el.closest('.event-field').style.display === 'none') return;
const v = el.value.trim();
values[f.id] = v;
if (f.pattern && v && !new RegExp('^' + f.pattern + '$').test(v)) {
err.textContent = t('Invalid') + ': ' + t(f.label);
err.classList.remove('hidden');
valid = false;
}
});
if (!valid) return;
try {
eventData = cfg.build(values);
} catch (e) {
err.textContent = 'Error: ' + e.message;
err.classList.remove('hidden');
return;
}
}
btn.disabled = true;
btn.textContent = t('Sending...');
try {
const resp = await pysimFetch('/api/event-send', { event_type: _eventFormType, event_data: eventData });
if (resp.sw === '9000') {
result.textContent = (eventData ? t('Event data sent.') : t('Event sent.')) + ' SW: ' + resp.sw;
result.classList.remove('hidden');
} else {
result.textContent = 'SW: ' + resp.sw;
result.classList.remove('hidden');
result.classList.add('text-amber-600');
}
} catch (e2) {
err.textContent = 'Error: ' + e2.message;
err.classList.remove('hidden');
} finally {
btn.disabled = false;
btn.textContent = t('Send');
}
}
async function pysimStatusPoll() {
const btn = document.getElementById('pli-status-btn');
if (!btn) return;
btn.disabled = true;
btn.textContent = t('STATUS...');
try {
const resp = await pysimFetch('/api/status-poll', {});
if (resp.fetch) {
btn.textContent = 'SW: ' + resp.sw + ' + ' + resp.fetch_sw;
} else {
btn.textContent = 'SW: ' + resp.sw;
}
pysimEventsRender();
pysimProactiveLogRender();
} catch (e) {
btn.textContent = t('Error');
} finally {
setTimeout(() => { btn.textContent = t('Send STATUS'); btn.disabled = false; }, 2000);
}
}
let _pysimPollTimer = null;
async function pysimPollToggle() {
const btn = document.getElementById('pli-pause-btn');
if (!btn) return;
try {
const status = await pysimFetch('/api/poll-status');
const newEnabled = !status.enabled;
const resp = await pysimFetch('/api/poll-toggle', { enabled: newEnabled });
pysimUpdatePollUI(resp.enabled, resp.interval);
} catch (e) { /* ignore */ }
}
function pysimUpdatePollUI(enabled, interval) {
const btn = document.getElementById('pli-pause-btn');
const stat = document.getElementById('pli-poll-stat');
if (!btn) return;
if (enabled) {
btn.textContent = 'ON';
btn.classList.remove('bg-gray-200', 'dark:bg-slate-700', 'text-gray-700', 'dark:text-slate-300');
btn.classList.add('bg-emerald-600', 'text-white');
} else {
btn.textContent = 'OFF';
btn.classList.remove('bg-emerald-600', 'text-white');
btn.classList.add('bg-gray-200', 'dark:bg-slate-700', 'text-gray-700', 'dark:text-slate-300');
}
if (stat) stat.textContent = enabled ? '(' + interval + 's)' : '';
}
async function pysimPollStatusInit() {
try {
const status = await pysimFetch('/api/poll-status');
pysimUpdatePollUI(status.enabled, status.interval);
} catch (e) { /* ignore */ }
}
function pysimStartBackendPoll() {
if (_pysimPollTimer) clearInterval(_pysimPollTimer);
let _hadData = false;
_pysimPollTimer = setInterval(async () => {
try {
const [log, stk, ps] = await Promise.all([
pysimFetch('/api/proactive-log'),
pysimFetch('/api/stk-status'),
pysimFetch('/api/poll-status'),
]);
pysimUpdatePollUI(ps.enabled, ps.interval);
if (Array.isArray(log) && log.length > 0) _hadData = true;
if (_hadData && !ps.enabled && Array.isArray(log) && log.length === 0) {
_hadData = false;
const statusEl = document.getElementById('pysim-status');
if (statusEl && statusEl.textContent.trim() !== '') {
statusEl.innerHTML = '<span class="text-red-500">Card disconnected</span>';
}
}
} catch (e) { /* ignore */ }
}, 5000);
}
// ===== proactive command log =====
const CMD_NAMES = {
'03': 'POLL INTERVAL', '05': 'SET UP EVENT LIST',
'13': 'SEND SHORT MESSAGE', '20': 'PLAY TONE',
'21': 'DISPLAY TEXT', '22': 'GET INKEY', '23': 'GET INPUT',
'24': 'SELECT ITEM', '25': 'SET UP MENU',
'26': 'PROVIDE LOCAL INFORMATION',
'15': 'LAUNCH BROWSER', '70': 'ACTIVATE',
};
const CMD_QUALIFIER_SHORT = {
'01': {0x00:'NAA Init+FC', 0x01:'File Chg', 0x02:'NAA Init+FC', 0x03:'NAA Init',
0x04:'UICC Reset', 0x05:'App Reset', 0x06:'Sess Reset', 0x07:'SoR', 0x08:'SoR I-WLAN'},
'10': {0x00:'IfNotBusy', 0x01:'IfNotBusy+Redial', 0x02:'HoldOthers', 0x03:'HoldOthers+Redial',
0x04:'DiscOthers', 0x05:'DiscOthers+Redial'},
'15': {0x00:'New', 0x01:'Existing', 0x02:'Close+New'},
'21': {0x00:'Normal', 0x01:'HighPri', 0x80:'WaitUser'},
'22': {0x00:'Digits', 0x01:'Alpha', 0x04:'YesNo', 0x08:'ImmDigit'},
'23': {0x00:'Digits+Echo', 0x01:'Alpha+Echo', 0x04:'NoEcho'},
'24': {0x00:'Normal', 0x02:'Help', 0x04:'DefaultItem', 0x80:'Nav'},
'25': {0x00:'Normal', 0x04:'Help', 0x80:'Nav'},
'26': {0x00:'Loc', 0x01:'IMEI', 0x02:'NMR', 0x03:'Time', 0x04:'Lang', 0x05:'TA',
0x06:'AccTech', 0x08:'IMEISV', 0x09:'Search', 0x0A:'Batt', 0x0C:'WSID',
0x0D:'BCInfo', 0x0E:'MultiAT', 0x0F:'MultiLoc', 0x10:'MultiNMR',
0x11:'CSG', 0x12:'HNB-IP', 0x13:'HNB-Macro', 0x14:'WLAN', 0x15:'Slices',
0x16:'CAG', 0x17:'RejSlice'},
};
let _pysimLogExpanded = new Set();
function pysimLogToggle(id) {
const panel = document.querySelector('[data-log-panel="' + id + '"]');
const tog = document.querySelector('[data-log-tog="' + id + '"]');
if (_pysimLogExpanded.has(id)) {
_pysimLogExpanded.delete(id);
if (panel) panel.classList.add('hidden');
if (tog) tog.textContent = '▶';
} else {
_pysimLogExpanded.add(id);
if (panel) panel.classList.remove('hidden');
if (tog) tog.textContent = '▼';
}
}
function pysimLogFields(decoded) {
if (!decoded || !decoded.length) return '';
let html = '';
decoded.forEach(f => {
html += '<div class="flex gap-1"><span class="text-gray-500 dark:text-slate-400 shrink-0">'
+ esc(f.label) + ':</span><span class="break-all">' + esc(f.value) + '</span></div>';
});
return html;
}
async function pysimProactiveLogRender() {
const el = document.getElementById('pysim-proactive-log');
el.innerHTML = '<span class="text-gray-400">' + t('Loading...') + '</span>';
try {
const log = await pysimFetch('/api/proactive-log');
if (!log || !log.length) {
el.innerHTML = '<span class="text-gray-400">' + t('No commands logged yet.') + '</span>';
return;
}
let html = '';
log.forEach((e, i) => {
const name = CMD_NAMES[e.type_hex] || ('Cmd 0x' + e.type_hex.toUpperCase());
let qual = '';
if (e.qualifier) {
const qs = CMD_QUALIFIER_SHORT[e.type_hex];
if (qs) {
const qv = qs[parseInt(e.qualifier,16)];
if (qv) qual = ' [' + qv + ']';
}
}
const expanded = _pysimLogExpanded.has(e.id);
html += '<div data-log-id="' + e.id + '" class="border-b border-gray-100 dark:border-slate-700/50">'
+ '<div class="flex items-center gap-2 py-0.5 cursor-pointer select-none" onclick="pysimLogToggle(' + e.id + ')">'
+ '<span class="w-12 text-right text-gray-400">' + e.elapsed + 's</span>'
+ '<span class="font-mono text-gray-400">0x' + e.type_hex.toUpperCase() + '</span>'
+ '<span class="truncate">' + esc(name) + '</span>'
+ (qual ? '<span class="text-xs text-gray-500 dark:text-slate-400">' + esc(qual) + '</span>' : '')
+ '<span class="ml-auto text-gray-400" data-log-tog="' + e.id + '">' + (expanded ? '▼' : '▶') + '</span>'
+ '</div>'
+ '<div data-log-panel="' + e.id + '" class="' + (expanded ? '' : 'hidden') + ' ml-8 pl-2 border-l border-gray-300 dark:border-slate-600 mb-1 text-xs space-y-0.5">'
+ '<div class="text-gray-500 dark:text-slate-400">' + t('Command') + ':</div>'
+ pysimLogFields(e.cmd_decoded);
if (e.raw) html += '<input type="text" readonly value="' + esc(e.raw) + '" class="w-full max-w-md font-mono text-xs rounded border border-gray-300 dark:border-slate-600 px-2 py-1 bg-gray-100 dark:bg-slate-800 text-gray-600 dark:text-slate-400">';
html += '<div class="text-gray-500 dark:text-slate-400 mt-1">' + t('Response') + ':</div>';
if (e.tr_hex !== undefined) {
if (e.tr_result !== undefined) {
html += '<div class="flex gap-1"><span class="text-gray-500 dark:text-slate-400 shrink-0">' + t('Result') + ':</span>'
+ '<span class="break-all">' + (e.tr_result_name ? esc(e.tr_result_name) + ' (' + esc(e.tr_result) + ')' : esc(e.tr_result)) + '</span></div>';
}
if (e.tr_hex) {
html += pysimLogFields(e.tr_decoded);
html += '<input type="text" readonly value="' + esc(e.tr_hex) + '" class="w-full max-w-md font-mono text-xs rounded border border-gray-300 dark:border-slate-600 px-2 py-1 bg-gray-100 dark:bg-slate-800 text-gray-600 dark:text-slate-400">';
}
} else {
html += '<div class="text-gray-400">' + t('No response yet') + '</div>';
}
html += '</div></div>';
});
el.innerHTML = html;
} catch (err) {
el.innerHTML = '<span class="text-red-500">Error: ' + esc(err.message) + '</span>';
}
}
// ===== PLI data dictionary =====
function decPlmn(hex3) {
const h = hex3.replace(/\s/g, '').slice(0, 6);
if (h.length < 6) return ['250', '01'];
const b1 = parseInt(h[0]+h[1], 16), b2 = parseInt(h[2]+h[3], 16), b3 = parseInt(h[4]+h[5], 16);
const mcc = '' + (b1&0xF) + ((b1>>4)&0xF) + (b2&0xF);
let mnc = '' + (b3&0xF);
if ((b2>>4) !== 0xF) mnc += ((b2>>4)&0xF);
return [mcc, mnc];
}
function decImei(hex8) {
const h = hex8.replace(/\s/g, '').slice(0, 16);
if (h.length < 16) return '';
let s = '';
for (let i = 0; i < 15; i += 2) {
const b = parseInt(h[i]+h[i+1], 16);
s += '' + (b&0xF) + ((b>>4)&0xF);
}
return s.slice(0, 15);
}
function encImeiPli(digits) {
const d = digits.replace(/\D/g, '').slice(0, 15);
if (d.length < 15) return '';
let h = '';
for (let i = 0; i < 15; i += 2)
h += (parseInt(d[i+1])<<4 | parseInt(d[i])).toString(16).padStart(2, '0');
return h;
}
function encImeisvPli(digits) {
const d = digits.replace(/\D/g, '').slice(0, 16);
if (d.length < 16) return '';
let h = '';
for (let i = 0; i < 16; i += 2)
h += (parseInt(d[i+1])<<4 | parseInt(d[i])).toString(16).padStart(2, '0');
return h;
}
const ACCESSTECH = [
{v:0,l:'GSM'},{v:1,l:'GSM Compact'},{v:2,l:'TIA/EIA-533'},{v:3,l:'UTRAN'},
{v:4,l:'TETRA'},{v:5,l:'TIA/EIA-95-B'},{v:6,l:'CDMA2000 1x'},{v:7,l:'CDMA2000 HRPD'},
{v:8,l:'E-UTRAN'},{v:9,l:'eHRPD'},{v:10,l:'NG-RAN'},{v:11,l:'Satellite NG-RAN'},{v:12,l:'Satellite E-UTRAN'},
];
const PLI_CODECS = {
'00': { tlv: '9305',
decode(hex) {
const v = hex.replace(/\s/g, '').slice(4); if (v.length < 10) return null;
const pl = decPlmn(v); return { mcc: pl[0], mnc: pl[1], lac: v.slice(6,10).toUpperCase() };
},
encode(v) { return '9305' + encPlmn(v.mcc||'250',v.mnc||'01') + ((v.lac||'0000').toUpperCase()); },
fields: [
{id:'mcc',label:'MCC',type:'text',pattern:'\\d{3}',placeholder:'e.g. 250'},
{id:'mnc',label:'MNC',type:'text',pattern:'\\d{2,3}',placeholder:'e.g. 01'},
{id:'lac',label:'LAC/TAC',type:'text',pattern:'[0-9A-Fa-f]{4}',placeholder:'hex'},
],
},
'01': { tlv: '9408',
decode(hex) {
const v = hex.replace(/\s/g, '').slice(4); if (v.length < 16) return null;
return { imei: decImei(v) };
},
encode(v) { const imei = encImeiPli(v.imei||''); return imei ? '9408'+imei : ''; },
fields: [
{id:'imei',label:'IMEI',type:'text',pattern:'\\d{15}',placeholder:'15 digits'},
],
},
'03': { tlv: '2607',
decode(hex) {
const v = hex.replace(/\s/g, '').slice(4); if (v.length < 14) return null;
const yr = parseInt(v[0]+v[1])<70 ? 2000+parseInt(v[0]+v[1]) : 1900+parseInt(v[0]+v[1]);
const mo = parseInt(v[2]+v[3]), dy = parseInt(v[4]+v[5]);
const hh = parseInt(v[6]+v[7]), mm = parseInt(v[8]+v[9]), ss = parseInt(v[10]+v[11]);
const tz = parseInt(v[12]+v[13], 16);
const tzSign = (tz & 0x80) ? '-' : '+';
const tzH = Math.floor(((tz & 0x3F) || 0) / 4);
const tzM = (((tz & 0x3F) || 0) % 4) * 15;
return {
dt: yr+'-'+(''+mo).padStart(2,'0')+'-'+(''+dy).padStart(2,'0'),
tm: (''+hh).padStart(2,'0')+':'+(''+mm).padStart(2,'0')+':'+(''+ss).padStart(2,'0'),
tz: tzSign+(''+tzH).padStart(2,'0')+':'+(''+tzM).padStart(2,'0'),
};
},
encode(v) {
const dt = (v.dt||'2000-01-01').split('-'), tm = (v.tm||'00:00:00').split(':');
let tzQ = 0;
const tzM = v.tz ? v.tz.match(/^([-+])(\d{2}):?(\d{2})$/) : null;
if (tzM) { const h = parseInt(tzM[2])||0, m = parseInt(tzM[3])||0; tzQ = h*4 + Math.floor(m/15); if (tzM[1]==='-') tzQ |= 0x80; }
return '2607'
+ (parseInt(dt[0])%100).toString(16).padStart(2,'0')
+ (parseInt(dt[1])||1).toString(16).padStart(2,'0')
+ (parseInt(dt[2])||1).toString(16).padStart(2,'0')
+ (parseInt(tm[0])||0).toString(16).padStart(2,'0')
+ (parseInt(tm[1])||0).toString(16).padStart(2,'0')
+ (parseInt(tm[2])||0).toString(16).padStart(2,'0')
+ tzQ.toString(16).padStart(2,'0');
},
fields: [
{id:'dt',label:'Date',type:'date'},{id:'tm',label:'Time',type:'time'},
{id:'tz',label:'TZ offset',type:'text',pattern:'[-+]\\d{2}:?\\d{2}',placeholder:'+00:00'},
],
},
'04': { tlv: 'AD02',
decode(hex) {
const v = hex.replace(/\s/g, '').slice(4); if (v.length < 4) return null;
return { lang: String.fromCharCode(parseInt(v[0]+v[1],16), parseInt(v[2]+v[3],16)) };
},
encode(v) {
const lang = (v.lang||'en').slice(0,2);
return 'AD02' + lang.charCodeAt(0).toString(16).padStart(2,'0') + lang.charCodeAt(1).toString(16).padStart(2,'0');
},
fields: [
{id:'lang',label:'Language',type:'text',pattern:'[a-zA-Z]{2,3}',placeholder:'en'},
],
},
'05': { tlv: 'C602',
decode(hex) {
const v = hex.replace(/\s/g, '').slice(4); if (v.length < 4) return null;
return { status: parseInt(v[0]+v[1],16).toString(), ta: parseInt(v[2]+v[3],16).toString() };
},
encode(v) { return 'C602' + (parseInt(v.status)||0).toString(16).padStart(2,'0') + (parseInt(v.ta)||0).toString(16).padStart(2,'0'); },
fields: [
{id:'status',label:'ME Status',type:'number',min:'0',max:'255',placeholder:'0'},
{id:'ta',label:'Timing Advance',type:'number',min:'0',max:'255',placeholder:'0'},
],
},
'06': { tlv: 'BF01',
decode(hex) {
const v = hex.replace(/\s/g, '').slice(4); if (v.length < 2) return null;
return { tech: parseInt(v[0]+v[1],16).toString() };
},
encode(v) { return 'BF01' + (parseInt(v.tech)||0).toString(16).padStart(2,'0'); },
fields: [{id:'tech',label:'Access Technology',type:'select',opts:ACCESSTECH}],
},
'08': { tlv: '9408',
decode(hex) {
const v = hex.replace(/\s/g, '').slice(4); if (v.length < 16) return null;
return { imeisv: decImei(v) + v.slice(14,16).toUpperCase() };
},
encode(v) {
const raw = (v.imeisv||'').replace(/\D/g, '').slice(0, 16);
if (raw.length < 16) return '';
let h = '';
for (let i = 0; i < 16; i += 2)
h += (parseInt(raw[i+1])<<4 | parseInt(raw[i])).toString(16).padStart(2,'0');
return '9408' + h;
},
fields: [{id:'imeisv',label:'IMEISV',type:'text',pattern:'\\d{16}',placeholder:'16 digits'}],
},
'09': { tlv: 'AD01',
decode(hex) {
const v = hex.replace(/\s/g, '').slice(4); if (v.length < 2) return null;
return { mode: parseInt(v[0]+v[1],16).toString() };
},
encode(v) {
return 'AD01' + (parseInt(v.mode)||0).toString(16).padStart(2,'0');
},
fields: [{id:'mode',label:'Search Mode',type:'select',
opts:[{v:0,l:'Automatic'},{v:1,l:'Manual'}]}],
},
'0A': { tlv: '6301',
decode(hex) {
const v = hex.replace(/\s/g, '').slice(4); if (v.length < 2) return null;
return { charge: parseInt(v[0]+v[1],16).toString() };
},
encode(v) { return '6301' + (parseInt(v.charge)||50).toString(16).padStart(2,'0'); },
fields: [{id:'charge',label:'Charge state (%)',type:'number',min:'0',max:'100',placeholder:'50'}],
},
'0E': { tlv: 'BF',
decode(hex) {
const v = hex.replace(/\s/g, '').slice(4); if (v.length < 2) return null;
const techs = []; for (let i = 0; i < v.length; i += 2) techs.push(parseInt(v[i]+v[i+1],16));
return { techs: techs.join(',') };
},
encode(v) {
const techs = (v.techs||'').split(',').filter(t => t !== '').map(t => parseInt(t)&0xFF);
if (!techs.length) return '';
const len = techs.length.toString(16).padStart(2,'0');
return 'BF' + len + techs.map(t => t.toString(16).padStart(2,'0')).join('');
},
fields: [
{id:'techs',label:'Access Technologies (comma-separated)',type:'text',
placeholder:'e.g. 3,8,10 (= UTRAN + E-UTRAN + NG-RAN)'},
],
},
};
async function pysimPliRender() {
const el = document.getElementById('pysim-pli-dict');
el.innerHTML = '<span class="text-gray-400">' + t('Loading...') + '</span>';
try {
const data = await pysimFetch('/api/pli-dict');
let html = '<div class="space-y-1">';
PLI_QUALIFIERS.forEach(q => {
const val = data[q.code] || '';
const hasCodec = !!PLI_CODECS[q.code];
html += '<div data-pli-row="' + q.code + '" class="py-0.5 border-b border-gray-200 dark:border-slate-700">'
+ '<div class="flex items-baseline gap-1.5">'
+ '<span class="w-6 shrink-0 text-right text-gray-400 text-xs">' + q.code + '</span>'
+ '<span class="flex-1 min-w-0 text-[13px] leading-snug">' + esc(q.name) + '</span>'
+ '</div>'
+ '<div class="flex items-center gap-1 mt-0.5 ml-8">'
+ '<input type="text" id="pli-v-' + q.code + '" value="' + esc(val) + '" placeholder="hex"'
+ ' class="flex-1 min-w-0 border border-gray-300 dark:border-slate-600 text-xs rounded px-1.5 py-1 dark:bg-slate-800 font-mono">';
if (hasCodec) {
html += '<span onclick="pysimPliDecodeRow(\'' + q.code + '\')" id="pli-tog-' + q.code + '"'
+ ' class="cursor-pointer select-none text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">▶</span>';
}
html += '</div><div class="pli-decode-panel hidden mt-1 ml-8 pl-2 border-l border-gray-300 dark:border-slate-600" data-pli-panel="' + q.code + '"></div></div>';
});
html += '</div>';
el.innerHTML = html;
} catch (e) {
el.innerHTML = '<span class="text-red-500">Error: ' + esc(e.message) + '</span>';
}
}
function pysimPliDecodeRow(code) {
const panel = document.querySelector('[data-pli-panel="' + code + '"]');
const codec = PLI_CODECS[code];
if (!codec) return;
if (!panel.classList.contains('hidden')) {
panel.innerHTML = '';
panel.classList.add('hidden');
const tog = document.getElementById('pli-tog-' + code);
if (tog) tog.textContent = '▶';
return;
}
const input = document.getElementById('pli-v-' + code);
const hex = input.value.trim().replace(/\s/g, '');
let values = {};
if (hex) {
values = codec.decode(hex);
if (!values) {
panel.innerHTML = '<span class="text-xs text-red-500">Invalid hex</span>';
panel.classList.remove('hidden');
const tog = document.getElementById('pli-tog-' + code);
if (tog) tog.textContent = '▼';
return;
}
}
let html = '<div class="space-y-1 mb-1">';
codec.fields.forEach(f => {
html += '<div class="flex items-center gap-1">'
+ '<span class="text-xs w-12 text-gray-500 dark:text-slate-400 truncate" title="' + esc(f.label) + '">' + esc(f.label) + '</span>';
if (f.type === 'select') {
html += '<select id="pli-d-' + code + '-' + f.id + '" class="text-xs border border-gray-300 dark:border-slate-600 rounded px-1.5 py-1 dark:bg-slate-800 w-40">';
f.opts.forEach(o => html += '<option value="' + o.v + '"' + (String(values[f.id]) === String(o.v) ? ' selected' : '') + '>' + esc(o.l) + '</option>');
html += '</select>';
} else {
html += '<input id="pli-d-' + code + '-' + f.id + '" type="' + f.type + '"'
+ ' value="' + esc(String(values[f.id] || '')) + '"'
+ (f.pattern ? ' pattern="' + f.pattern + '"' : '')
+ (f.placeholder ? ' placeholder="' + f.placeholder + '"' : '')
+ (f.min !== undefined ? ' min="' + f.min + '" max="' + f.max + '"' : '')
+ ' class="text-xs border border-gray-300 dark:border-slate-600 rounded px-1.5 py-1 dark:bg-slate-800 w-40 font-mono">';
}
html += '</div>';
});
html += '</div>';
html += '<div class="flex gap-1">'
+ '<button onclick="pysimPliApplyRow(\'' + code + '\')" class="text-xs px-2 py-1 rounded bg-emerald-600 text-white hover:bg-emerald-700">Apply</button>'
+ '<button onclick="var p=document.querySelector(\'[data-pli-panel=\\\'' + code + '\\\']\');p.classList.add(\'hidden\');p.innerHTML=\'\';var t=document.getElementById(\'pli-tog-' + code + '\');if(t)t.textContent=\'▶\'" class="text-xs px-2 py-1 rounded border border-gray-300 dark:border-slate-600">Cancel</button>'
+ '</div>';
panel.innerHTML = html;
panel.classList.remove('hidden');
const tog = document.getElementById('pli-tog-' + code);
if (tog) tog.textContent = '▼';
}
function pysimPliApplyRow(code) {
const codec = PLI_CODECS[code];
if (!codec) return;
const values = {};
codec.fields.forEach(f => {
const el = document.getElementById('pli-d-' + code + '-' + f.id);
if (el) values[f.id] = el.value;
});
const hex = codec.encode(values);
if (!hex) return;
const input = document.getElementById('pli-v-' + code);
if (input) input.value = hex;
const panel = document.querySelector('[data-pli-panel="' + code + '"]');
if (panel) { panel.innerHTML = ''; panel.classList.add('hidden'); }
const tog = document.getElementById('pli-tog-' + code);
if (tog) tog.textContent = '▶';
}
async function pysimPliSave() {
const btn = document.getElementById('pli-save-btn');
btn.disabled = true;
btn.textContent = t('Saving...');
const data = {};
PLI_QUALIFIERS.forEach(q => {
const el = document.getElementById('pli-v-' + q.code);
if (el) {
const v = el.value.trim().replace(/\s/g, '');
if (v && !/^[0-9A-Fa-f]*$/.test(v)) return;
data[q.code] = v;
}
});
try {
await pysimFetch('/api/pli-dict', data);
btn.textContent = t('Saved');
btn.classList.add('bg-emerald-700');
setTimeout(() => { btn.textContent = t('Save'); btn.classList.remove('bg-emerald-700'); btn.disabled = false; }, 1500);
} catch (e) {
btn.textContent = 'Error';
btn.classList.add('bg-red-600');
setTimeout(() => { btn.textContent = t('Save'); btn.classList.remove('bg-red-600'); btn.disabled = false; }, 2000);
}
}
// ===== custom files =====
let pysimCustomFiles = [];
function pysimCustomLoad() {
try {
const d = localStorage.getItem('pysim_custom_files');
pysimCustomFiles = d ? JSON.parse(d) : [];
} catch (e) { pysimCustomFiles = []; }
}
function pysimCustomSave() {
localStorage.setItem('pysim_custom_files', JSON.stringify(pysimCustomFiles));
}
function pysimCustomAdd() {
const pathEl = document.getElementById('pysim-cf-path');
const nameEl = document.getElementById('pysim-cf-name');
const path = pathEl.value.trim().toUpperCase();
const name = nameEl.value.trim();
if (!path || !name) return;
if (!name.startsWith('EF.') && !name.startsWith('DF.')) {
alert('Alias must start with "EF." or "DF."');
return;
}
const parts = path.split('/').filter(Boolean);
if (parts.length < 2) { alert('Path must have at least 2 parts, e.g. 3F00/6F46'); return; }
const fid = parts[parts.length - 1];
const parentFid = parts[parts.length - 2];
if (pysimCustomFiles.some(c => c.path.toUpperCase() === path)) { alert('Path already exists'); return; }
pysimCustomFiles.push({ path, name, fid, parentFid });
pysimCustomSave();
pysimCustomRender();
pathEl.value = '';
nameEl.value = '';
}
function pysimCustomRemove(i) {
pysimCustomFiles.splice(i, 1);
pysimCustomSave();
pysimCustomRender();
}
function pysimCustomRender() {
const el = document.getElementById('pysim-cf-list');
if (!pysimCustomFiles.length) { el.innerHTML = '<span class="text-gray-400">No custom files defined.</span>'; return; }
let html = '';
for (let i = 0; i < pysimCustomFiles.length; i++) {
const c = pysimCustomFiles[i];
html += '<div class="flex items-center gap-2 py-1 border-b border-gray-200 dark:border-slate-700">';
html += '<span class="text-gray-500">' + esc(c.path) + '</span> <span class="text-blue-600 dark:text-blue-400">→</span> <b>' + esc(c.name) + '</b>';
html += '<span onclick="pysimCustomRemove(' + i + ')" class="ml-auto cursor-pointer text-red-400 hover:text-red-600">✕</span>';
html += '</div>';
}
el.innerHTML = html;
}
function pysimCustomExport() {
const el = document.getElementById('pysim-cf-io');
el.value = JSON.stringify(pysimCustomFiles, null, 2);
el.classList.remove('hidden');
el.select();
}
function pysimCustomExportFile() {
downloadJson('otaman-custom-files.json', pysimCustomFiles);
}
async function pysimCustomImport(text) {
try {
if (text == null) text = await navigator.clipboard.readText();
const imported = JSON.parse(text);
if (!Array.isArray(imported)) throw new Error('Expected an array');
let added = 0;
for (const entry of imported) {
if (!entry.path || !entry.name) continue;
const path = entry.path.toUpperCase();
if (!pysimCustomFiles.some(c => c.path.toUpperCase() === path)) {
const parts = path.split('/').filter(Boolean);
pysimCustomFiles.push({
path: path,
name: entry.name.toUpperCase(),
fid: (entry.fid || parts[parts.length - 1]).toUpperCase(),
parentFid: (entry.parentFid || parts[parts.length - 2]).toUpperCase(),
});
added++;
}
}
pysimCustomSave();
pysimCustomRender();
const el = document.getElementById('pysim-cf-io');
el.value = 'Imported ' + added + ' files.';
setTimeout(() => { el.classList.add('hidden'); }, 2000);
} catch (e) {
alert('Import error: ' + e.message);
}
}
function pysimCustomImportFile(input) {
const file = input.files && input.files[0];
input.value = '';
if (!file) return;
const reader = new FileReader();
reader.onload = () => pysimCustomImport(reader.result);
reader.readAsText(file);
}
function pysimCustomImportField() {
const el = document.getElementById('pysim-cf-io');
const val = el.value.trim();
if (!el.classList.contains('hidden') && val.startsWith('[')) { pysimCustomImport(val); return; }
el.value = '';
el.placeholder = 'Paste JSON array here, then press Paste & import again';
el.classList.remove('hidden');
el.focus();
}
// Init custom files
pysimCustomLoad();
// Init sub-tab pills
document.querySelectorAll('.pysim-subtab').forEach(btn => {
btn.addEventListener('click', () => pysimSwitchSubtab(btn.dataset.pysimSub));
});
// ===== i18n =====
const LANG_RU = {
'Generate APDU': 'Сгенерировать APDU',
'Pack into Secured packet': 'Упаковать в Secured packet',
'Convert': 'Конвертировать',
'Decode': 'Декодировать',
'Generate Expanded Script APDU': 'Сгенерировать Expanded Script APDU',
'Generate Secure Packet': 'Сгенерировать Secure Packet',
'Conversion': 'Конвертация',
'Command': 'Команда',
'Response': 'Ответ',
'Result': 'Результат',
'No response yet': 'Ответа пока нет',
'Start with SELECT': 'Начать с SELECT',
'SELECT method': 'Метод SELECT',
'Path (hex)': 'Путь (hex)',
'FID chain (comma-separated)': 'FID цепочка (через запятую)',
'Record #': 'Запись №',
'Selection mode (P2)': 'Режим выборки (P2)',
'Offset (hex)': 'Смещение (hex)',
'Data (hex)': 'Данные (hex)',
'Record size (bytes)': 'Размер записи (байт)',
'Allow P1/P2 editing': 'Разрешить редактирование P1/P2',
'Privileges': 'Привилегии',
'PIN value': 'Значение PIN',
'Old PIN': 'Старый PIN',
'New PIN': 'Новый PIN',
'ELF AID (hex)': 'ELF AID (hex)',
'Module AID (hex)': 'AID модуля (hex)',
'Security level (P1)': 'Уровень безопасности (P1)',
'Target': 'Цель',
'Current EF (case 1)': 'Текущий EF (case 1)',
'By FID': 'По FID',
'Path from MF': 'Путь от MF',
'Path from current DF': 'Путь от текущего DF',
'Unblock PIN': 'Разблокировка PIN',
'Byte 1': 'Байт 1',
'Byte 2': 'Байт 2',
'SIM/UICC Toolkit parameters': 'Параметры SIM/UICC Toolkit',
'Toolkit mode': 'Режим Toolkit',
'Max services': 'Максимум сервисов',
'Priority': 'Приоритет',
'Timers (max)': 'Таймеры (макс)',
'Menu text length': 'Длина текста меню',
'Menu items': 'Пункты меню',
'First menu position': 'Первая позиция в меню',
'First menu ID (hex)': 'ID первого пункта меню (hex)',
'Last menu position': 'Позиция последнего пункта меню',
'Last menu ID (hex)': 'ID последнего пункта меню (hex)',
'Channels (max)': 'Каналы (макс)',
'Access domain (hex)': 'Домен доступа (hex)',
'Load data (hex)': 'Данные загрузки (hex)',
'Block number': 'Номер блока',
'Encryption': 'Шифрование',
'DELETE mode': 'Режим DELETE',
'GET STATUS mode': 'Режим GET STATUS',
'GET STATUS format': 'Формат GET STATUS',
'SET STATUS type': 'Тип SET STATUS',
'State': 'Состояние',
'GET DATA tag': 'Tag GET DATA',
'Response data (hex)': 'Данные ответа (hex)',
'— Select command —': '— Выберите команду —',
'— select preset —': '— выберите предустановленный —',
'SIM OTA with a Human Face': 'SIM OTA с человеческим лицом',
'Error: specify SELECT parameters': 'Ошибка: укажите параметры SELECT',
'Error: unknown command': 'Ошибка: неизвестная команда',
'Error: specify data': 'Ошибка: укажите данные',
'Error: specify SD AID': 'Ошибка: укажите SD AID',
'Error: specify AID': 'Ошибка: укажите AID',
'Error: specify data (host cryptogram)': 'Ошибка: укажите данные (host cryptogram)',
'Error: specify data (challenge)': 'Ошибка: укажите данные (challenge)',
'Error: enter a value': 'Ошибка: введите значение',
'Error: IMSI must be 15 digits': 'Ошибка: IMSI должен быть 15 цифр',
'Error: specify MCC and MNC': 'Ошибка: укажите MCC и MNC',
'Error: MCC must be 3 digits': 'Ошибка: MCC должен быть 3 цифры',
'Error: MNC must be 2-3 digits': 'Ошибка: MNC должен быть 2-3 цифры',
'Error: enter an even number of hex characters': 'Ошибка: введите чётное количество hex-символов',
'Error: specify APDU': 'Ошибка: укажите APDU',
'KIc key error: ': 'Ошибка KIc key: ',
'KID key error: ': 'Ошибка KID key: ',
'Error: ciphering requires KIc key': 'Ошибка: ciphering requires KIc key',
'Error: MAC requires KID key': 'Ошибка: MAC requires KID key',
'Response parser': 'Парсер ответов',
'C-APDU Parser': 'Разбор C-APDU',
'Cards': 'Карты',
'Card reader': 'Картридер',
'Card presets are stored locally in your browser. JSON export/import buttons are below the card list.': 'Данные карт хранятся локально в браузере. Кнопки экспорта/импорта JSON — под списком карт.',
'Add': 'Добавить',
'Export as JSON': 'Экспорт в JSON',
'Export to file': 'Экспорт в файл',
'Import from file': 'Импорт из файла',
'Paste & import': 'Вставить и импортировать',
'Import JSON from clipboard': 'Импорт JSON из буфера',
'Name': 'Имя',
'ICCID': 'ICCID',
'Cntr': 'Сч',
'Remove': 'Удалить',
'No cards defined.': 'Карты не заданы.',
'Card presets': 'Выбор карты',
'— Select card —': '— Выберите карту —',
'Server URL': 'URL сервера',
'Connect': 'Подключиться',
'To access local card reader, you need pysim-otaman-server running locally.': 'Для доступа к локальному картридеру необходим запущенный pysim-otaman-server.',
'To connect, install': 'Для подключения установите',
'and start it.': 'и запустите его.',
'Refer to pysim-otaman-server README files for details.': 'Подробности — в README файлах pysim-otaman-server.',
'Click': 'Нажмите кнопку',
'when pysim-otaman-server is ready': 'когда pysim-otaman-server будет готов',
'Equip card': 'Подключить карту',
'Check status': 'Проверить статус',
'Reset card': 'Сброс карты',
'Disconnect from pySim': 'Отключиться от pySim',
'File manager': 'Файловый менеджер',
'Custom files': 'Пользовательские файлы',
'pySim command line': 'Командная строка pySim',
'Raw APDU': 'Отправка APDU',
'Read': 'Прочитать',
'Save': 'Сохранить',
'Raw': 'Данные как на карте',
'Decoded': 'Декодированные данные',
'Edit': 'Редактировать',
'Execute': 'Выполнить',
'Send': 'Отправить',
'Add known file paths and aliases to the file tree.': 'Добавьте пути и псевдонимы файлов в дерево файлов.',
'File:': 'Файл:',
'Cancel': 'Отмена',
'Timeout': 'Таймаут',
'OK': 'OK',
'Exit': 'Выход',
'← Back': '← Назад',
'Connecting...': 'Подключение...',
'Checking card...': 'Проверка карты...',
'Connected': 'Подключено',
'Connection failed:': 'Ошибка подключения:',
'pysim-blocked-hint': 'Браузер, вероятно, заблокировал доступ к локальному серверу карт. Разрешите доступ к локальной сети для этого сайта (Chrome/Edge/Vivaldi: Настройки сайта → Доступ к локальной сети → Разрешить). Также проверьте, что pysim-otaman-server запущен по адресу {url}.',
'pysim-unreachable-hint': 'Не удалось подключиться к серверу карт. Проверьте, что pysim-otaman-server запущен и адрес указан верно ({url}). Если эта страница открыта по HTTPS, также разрешите доступ к локальной сети в браузере (Chrome/Edge/Vivaldi: Настройки сайта → Доступ к локальной сети → Разрешить).',
'No card detected. Insert card and click Equip card button': 'Карта не обнаружена. Вставьте карту и нажмите Подключить карту',
'Checking...': 'Проверка...',
'Resetting...': 'Сброс...',
'Equipping...': 'Подключение...',
'Sending OTA...': 'Отправка OTA...',
'Verifying against pySim reference...': 'Сверка с эталоном pySim...',
'Verify vs pySim': 'Проверить в pySim',
'Send to Card': 'Отправить на карту',
'Proactive UICC': 'Проактивный UICC',
'Events that the card monitors (SET UP EVENT LIST):': 'События, отслеживаемые картой (SET UP EVENT LIST):',
'No events configured.': 'Нет настроенных событий.',
'Fetched proactive commands:': 'Извлечённые проактивные команды:',
'No commands logged yet.': 'Команды пока не регистрировались.',
'Send event notification to the card?': 'Отправить уведомление о событии на карту?',
'This event type is not yet supported.': 'Этот тип события пока не поддерживается.',
'Event sent.': 'Событие отправлено.',
'Event data sent.': 'Данные события отправлены.',
'Sending...': 'Отправка...',
'Invalid': 'Некорректное значение',
'Service Status': 'Статус сервиса',
'Normal service': 'Обычный сервис',
'Limited service': 'Ограниченный сервис',
'No service': 'Нет сервиса',
'Language code': 'Код языка',
'Registration type': 'Тип регистрации',
'Access Technology': 'Технология доступа',
'Rejection cause code': 'Код причины отклонения',
'Extended EMM cause (optional)': 'Расширенная EMM причина (опционально)',
'Status': 'Статус',
'Card reader removed': 'Картридер извлечён',
'Card reader inserted': 'Картридер вставлен',
'PROVIDE LOCAL INFORMATION response data': 'Данные для PROVIDE LOCAL INFORMATION',
'Send STATUS': 'Отправить STATUS',
'help': 'справка',
'STATUS and Polling': 'STATUS и опрос',
'Polling': 'Опрос',
'STATUS...': 'STATUS...',
'Saving...': 'Сохранение...',
'Saved': 'Сохранено',
'Save': 'Сохранить',
};
let currentLang = 'en';
function t(text) {
if (currentLang === 'ru' && LANG_RU[text]) return LANG_RU[text];
return text;
}
function toggleLang() {
currentLang = currentLang === 'en' ? 'ru' : 'en';
localStorage.setItem('lang', currentLang);
document.getElementById('lang-btn').textContent = currentLang.toUpperCase();
translatePage();
translatePage();
updateHelpLink();
}
let currentHelpAnchor = 'sim-rfm';
let pysimHelpAnchor = 'file-manager';
function updateHelpLink() {
const link = document.getElementById('help-link');
if (link) link.href = (currentLang === 'ru' ? 'help-ru.html' : 'help.html') + '#' + currentHelpAnchor;
}
function setHelpAnchor(anchor) {
currentHelpAnchor = anchor;
updateHelpLink();
}
function translatePage() {
document.querySelectorAll('[data-l10n]').forEach(el => {
const key = el.getAttribute('data-l10n');
el.textContent = t(key);
});
const slogan = document.getElementById('slogan');
if (slogan) slogan.textContent = t('SIM OTA with a Human Face');
}
// Init language
const userLang = (navigator.language || '').split('-')[0];
currentLang = localStorage.getItem('lang') || (userLang === 'ru' ? 'ru' : 'en');
document.getElementById('lang-btn').textContent = currentLang.toUpperCase();
updateHelpLink();
cardsLoad();
if (localStorage.getItem('theme') === 'dark' ||
(!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
function toggleTheme() {
const html = document.documentElement;
html.classList.toggle('dark');
localStorage.setItem('theme', html.classList.contains('dark') ? 'dark' : 'light');
document.getElementById('theme-btn').textContent = html.classList.contains('dark') ? '☀️' : '🌙';
}
document.getElementById('theme-btn').textContent = document.documentElement.classList.contains('dark') ? '☀️' : '🌙';
updateConvFields();
updateSp();
updateSpKic();
updateSpKid();
updateSimUsimFields('sim');
updateSimSelectMethod('sim');
updateSimSelectSection('sim');
toggleSimOverride('sim');
toggleSimOverride('usim');
updateP1P2Display('sim');
updateP1P2Display('usim');
updateRamFields();
translatePage();
// PWA
let deferredPrompt;
const installBtn = document.getElementById('install-btn');
if (!window.matchMedia('(display-mode: standalone)').matches) {
window.addEventListener('beforeinstallprompt', e => {
e.preventDefault();
deferredPrompt = e;
installBtn.style.display = '';
});
installBtn.addEventListener('click', async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') deferredPrompt = null;
});
window.addEventListener('appinstalled', () => { installBtn.style.display = 'none'; });
}
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js');
}
</script>
</body>
</html>