3066 lines
147 KiB
HTML
3066 lines
147 KiB
HTML
<!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/png" href="sim.png">
|
||
<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>
|
||
</head>
|
||
<body class="bg-neutral-50 dark:bg-slate-900 text-gray-800 dark:text-slate-200">
|
||
|
||
<div class="max-w-4xl mx-auto px-6 py-4">
|
||
|
||
<div class="flex items-center justify-between mb-6">
|
||
<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.0.0</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>
|
||
<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-2 text-sm rounded-t bg-blue-600 dark:bg-blue-500 text-white dark:text-white" data-tab="sim">SIM RFM</button>
|
||
<button class="tab-btn px-4 py-2 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="usim">USIM RFM</button>
|
||
<button class="tab-btn px-4 py-2 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="ber">BER-TLV</button>
|
||
<button class="tab-btn px-4 py-2 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="ram">RAM</button>
|
||
<button class="tab-btn px-4 py-2 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="sp">Secured Packet</button>
|
||
<button class="tab-btn px-4 py-2 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">Response parser</button>
|
||
<button class="tab-btn px-4 py-2 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">Card reader</button>
|
||
</div>
|
||
|
||
<div id="tab-sim" class="tab-content">
|
||
<div class="flex gap-4 mb-3">
|
||
<div class="w-2/3">
|
||
<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-2.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>
|
||
</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-2.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-2.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-2.5 dark:bg-slate-800" placeholder="3F007FFF6FC5">
|
||
</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-2.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-2.5 dark:bg-slate-800" placeholder="3F00,6F3A">
|
||
</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-2.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-2.5 dark:bg-slate-800">
|
||
<option value="04">Absolute (04)</option>
|
||
<option value="06">Next (06)</option>
|
||
<option value="02">Previous (02)</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-2.5 dark:bg-slate-800" placeholder="0000" maxlength="4" value="0000">
|
||
</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-2.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-2.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-2.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-2.5 dark:bg-slate-800">
|
||
<option value="01">PIN1 (01)</option>
|
||
<option value="02">PIN2 (02)</option>
|
||
</select>
|
||
</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-2.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-2.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 class="w-1/3">
|
||
<div class="border border-gray-300 dark:border-slate-600 rounded p-3 bg-gray-50 dark:bg-slate-800">
|
||
<h3 class="text-sm font-medium text-gray-700 dark:text-slate-300 mb-2" data-l10n="Conversion">Conversion</h3>
|
||
<div class="mb-2">
|
||
<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">
|
||
<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>
|
||
<div class="mb-2">
|
||
<input id="conv-input" 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">
|
||
</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>
|
||
<button onclick="genConvert()" class="w-full mb-2 px-3 py-1.5 bg-blue-600 text-white text-xs font-medium rounded hover:bg-blue-700" data-l10n="Convert">Convert</button>
|
||
<textarea id="conv-result" rows="2" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1.5 bg-gray-100 dark:bg-slate-800"></textarea>
|
||
</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-2.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="tab-usim" class="tab-content hidden">
|
||
<div class="flex gap-4 mb-3">
|
||
<div class="w-2/3">
|
||
<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-2.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>
|
||
</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-2.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-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" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.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" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800" placeholder="3F007FFF6FC5">
|
||
</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-2.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-2.5 dark:bg-slate-800" placeholder="3F00,6F3A">
|
||
</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-2.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-2.5 dark:bg-slate-800">
|
||
<option value="04">Absolute (04)</option>
|
||
<option value="06">Next (06)</option>
|
||
<option value="02">Previous (02)</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-2.5 dark:bg-slate-800" placeholder="0000" maxlength="4" value="0000">
|
||
</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-2.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-2.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-2.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-2.5 dark:bg-slate-800">
|
||
<option value="01">PIN1 (01)</option>
|
||
<option value="02">PIN2 (02)</option>
|
||
</select>
|
||
</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-2.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-2.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 class="w-1/3">
|
||
<div class="border border-gray-300 dark:border-slate-600 rounded p-3 bg-gray-50 dark:bg-slate-800">
|
||
<h3 class="text-sm font-medium text-gray-700 dark:text-slate-300 mb-2" data-l10n="Conversion">Conversion</h3>
|
||
<div class="mb-2">
|
||
<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">
|
||
<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>
|
||
<div class="mb-2">
|
||
<input id="conv-input" 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">
|
||
</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>
|
||
<button onclick="genConvert()" class="w-full mb-2 px-3 py-1.5 bg-blue-600 text-white text-xs font-medium rounded hover:bg-blue-700" data-l10n="Convert">Convert</button>
|
||
<textarea id="conv-result" rows="2" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1.5 bg-gray-100 dark:bg-slate-800"></textarea>
|
||
</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-2.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="tab-ber" class="tab-content 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-2.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 BER-TLV">Generate BER-TLV</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-2.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="tab-ram" 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="ram-cmd" onchange="updateRamFields()" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.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-2.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-2.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-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-2.5 dark:bg-slate-800">
|
||
<option value="ca">SIM Toolkit (Tag CA)</option>
|
||
<option value="80">UICC Toolkit (Tag 80)</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>
|
||
</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-2.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-2.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-2.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-2.5 dark:bg-slate-800">
|
||
<option value="00">No encryption, last block</option>
|
||
<option value="40">No encryption, more blocks</option>
|
||
<option value="80">With encryption, last block</option>
|
||
<option value="C0">With encryption, more blocks</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-2.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-2.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-2.5 dark:bg-slate-800">
|
||
<option value="40">40 — First/all, GP TLV format</option>
|
||
<option value="42">42 — Next, GP TLV format</option>
|
||
<option value="00">00 — First/all, old format (deprecated)</option>
|
||
<option value="02">02 — Next, old 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-2.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-2.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-2.5 dark:bg-slate-800">
|
||
<option value="80">ISD (80)</option>
|
||
<option value="40">Application or SSD (40)</option>
|
||
<option value="60">SD and its applications (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-2.5 dark:bg-slate-800">
|
||
<option value="00">Unlock (previous state)</option>
|
||
<option value="80">Lock (LOCKED)</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-2.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="tab-sp" class="tab-content hidden">
|
||
<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-2.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="12">12 — CC/MAC + Counter (available)</option>
|
||
<option value="16">16 — CC/MAC + Ciphering + Counter (available)</option>
|
||
<option value="22">22 — CC/MAC + Counter (check higher)</option>
|
||
<option value="26">26 — CC/MAC + Ciphering + Counter (check higher)</option>
|
||
<option value="32">32 — CC/MAC + Counter (check +1)</option>
|
||
<option value="36">36 — CC/MAC + Ciphering + Counter (check +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-2.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-2.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-2.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-2.5 dark:bg-slate-800">
|
||
<option value="01">01 — DES-CBC (8B 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-2.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-2.5 dark:bg-slate-800">
|
||
<option value="01">01 — DES-CBC (8B 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" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.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">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" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.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" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.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" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.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" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.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" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.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>
|
||
<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-2.5 bg-gray-100 dark:bg-slate-800"></textarea>
|
||
</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-2.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-2.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-2.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-2.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-2.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">Server URL</label>
|
||
<div class="flex gap-2 items-center">
|
||
<input id="pysim-url" 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" 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">Connect</button>
|
||
</div>
|
||
</div>
|
||
<div id="pysim-connect-info" class="mb-3 text-xs text-gray-500 dark:text-slate-400 leading-relaxed">
|
||
<p>To access local card reader, you need pySim running locally.</p>
|
||
<p>To connect, install <a href="https://github.com/anttro/pysim-otaman-server" 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/pysim-otaman-server.git</code> and start it. Refer to README.md for details. Click <b>Connect</b> once pysim-otaman-server is ready.</p>
|
||
</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">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">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">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">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-xs rounded-full bg-blue-600 text-white" data-pysim-sub="files">File manager</button>
|
||
<button class="pysim-subtab 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-pysim-sub="cmd">pySim command line</button>
|
||
<button class="pysim-subtab 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-pysim-sub="apdu">Raw APDU</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">File: <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()" class="px-2 py-1 text-xs rounded bg-blue-600 text-white hover:bg-blue-700">Read</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')">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')">Decoded</span>
|
||
</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-cmd" class="hidden">
|
||
<div class="flex gap-2 items-center mb-2">
|
||
<input id="pysim-cmd" 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="select MF" autocomplete="off" list="pysim-cmdlist" onkeydown="pysimCmdKeydown(event)">
|
||
<datalist id="pysim-cmdlist"></datalist>
|
||
<button onclick="pysimExec()" class="px-4 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700">Execute</button>
|
||
</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">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>
|
||
|
||
</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 === 'sim' || name === 'usim') {
|
||
updateSimUsimFields(name);
|
||
updateSimSelectMethod(name);
|
||
}
|
||
}
|
||
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', 'verify', 'change']);
|
||
const RECSIZE_OPS = new Set(['update-record']);
|
||
const PIN_OPS = new Set(['verify', 'change']);
|
||
|
||
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';
|
||
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 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: 0x09, selP2: 0x0C};
|
||
const cmd = document.getElementById(mode + '-cmd').value;
|
||
if (cmd === 'select') {
|
||
document.getElementById(mode + '-p1').value = preset.selP1.toString(16).padStart(2, '0').toUpperCase();
|
||
document.getElementById(mode + '-p2').value = preset.selP2.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 = 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') {
|
||
p1 = 0x00; p2 = 0x00;
|
||
} else if (cmd === 'verify' || cmd === 'change') {
|
||
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;
|
||
document.getElementById('ram-priv-row').style.display = cmd === 'install-install' ? '' : 'none';
|
||
document.getElementById('ram-toolkit-row').style.display = cmd === 'install-install' ? '' : 'none';
|
||
document.getElementById('ram-sdaid-row').style.display = cmd === 'install-load' ? '' : '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-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';
|
||
document.getElementById('ram-tk-ad-row').style.display =
|
||
document.getElementById('ram-tk-mode').value === 'ca' ? '' : 'none';
|
||
}
|
||
document.getElementById('ram-tk-mode').addEventListener('change', () => {
|
||
document.getElementById('ram-tk-ad-row').style.display =
|
||
document.getElementById('ram-tk-mode').value === 'ca' ? '' : 'none';
|
||
});
|
||
|
||
// ===== 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() +
|
||
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) + '00';
|
||
}
|
||
|
||
function buildOpNoCla(ins, p1, p2, p3, data) {
|
||
let s = ins.toString(16).padStart(2, '0').toUpperCase() +
|
||
p1.toString(16).padStart(2, '0').toUpperCase() +
|
||
p2.toString(16).padStart(2, '0').toUpperCase() +
|
||
p3.toString(16).padStart(2, '0').toUpperCase();
|
||
if (data) s += data.toUpperCase();
|
||
return s;
|
||
}
|
||
|
||
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},
|
||
};
|
||
|
||
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) {
|
||
if (!selInfo || !selInfo.value) return '';
|
||
if (selInfo.type === 'fid') {
|
||
if (selInfo.value.length !== 4) return '';
|
||
return buildSelect(preset.cla, preset.selP1, preset.selP2, selInfo.value);
|
||
} else if (selInfo.type === 'path') {
|
||
if (selInfo.value.length < 2 || selInfo.value.length % 2 !== 0) return '';
|
||
const p3 = selInfo.value.length / 2;
|
||
return buildApdu(preset.cla, 0xA4, 0x08, 0x00, p3, selInfo.value) + '00';
|
||
} else if (selInfo.type === 'dfname') {
|
||
if (selInfo.value.length < 2) return '';
|
||
const p3 = selInfo.value.length / 2;
|
||
return buildApdu(preset.cla, 0xA4, 0x04, 0x00, p3, selInfo.value) + '00';
|
||
} else if (selInfo.type === 'chain') {
|
||
const fids = selInfo.value.split(',').map(f => f.trim().replace(/[^0-9a-fA-F]/g, '')).filter(f => f.length === 4);
|
||
if (fids.length === 0) return '';
|
||
return fids.map(fid => buildSelect(preset.cla, preset.selP1, preset.selP2, fid)).join('');
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function genSimUsim(mode) {
|
||
const preset = mode === 'sim' ? {cla: 0xA0, selP1: 0x00, selP2: 0x00} : {cla: 0x00, selP1: 0x09, selP2: 0x0C};
|
||
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 = 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 = 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') {
|
||
p1 = 0x00; p2 = 0x00; p3 = 0x00;
|
||
} else if (cmd === 'verify' || cmd === 'change') {
|
||
if (!dataHex) { resultEl.value = 'Error: specify data'; return; }
|
||
p1 = 0x00; p2 = parseInt(pin, 16); p3 = dataHex.length / 2; data = dataHex;
|
||
}
|
||
|
||
if (override) {
|
||
if (overrideP1 !== null) p1 = overrideP1;
|
||
if (overrideP2 !== null) p2 = overrideP2;
|
||
}
|
||
|
||
let apdu = '';
|
||
if (doSelect) {
|
||
const selectApdu = buildSelectApdu(mode, preset, selInfo);
|
||
if (selectApdu) {
|
||
apdu = selectApdu + buildOpNoCla(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 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 = document.getElementById('ram-ss-mode').value;
|
||
const ssState = document.getElementById('ram-ss-state').value;
|
||
|
||
const resultEl = document.getElementById('ram-result');
|
||
|
||
if (cmd === 'install-load' && !sdAid) { resultEl.value = 'Error: specify SD AID'; return; }
|
||
|
||
const INSTALL_P1 = {
|
||
'install-load': 0x02,
|
||
'install-install': 0x0C,
|
||
'install-make-sel': 0x10,
|
||
'install-reg-update': 0x01,
|
||
'install-extradition': 0x04,
|
||
};
|
||
|
||
let apdu = '';
|
||
if (cmd === 'install-load') {
|
||
const sdAidLen = (sdAid.length / 2).toString(16).padStart(2, '0');
|
||
const aidLen = (aid.length / 2).toString(16).padStart(2, '0');
|
||
let dataField = 'C9' + sdAidLen + sdAid + '5F50' + aidLen + aid;
|
||
if (loadData) {
|
||
const ldLen = (loadData.length / 2).toString(16).padStart(2, '0');
|
||
dataField += 'C4' + ldLen + loadData;
|
||
}
|
||
const dataLen = (dataField.length / 2).toString(16).padStart(2, '0');
|
||
apdu = buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dataLen, 16), dataField);
|
||
} else if (cmd === 'install-install') {
|
||
const aidLen = (aid.length / 2).toString(16).padStart(2, '0');
|
||
let dataField = '4F' + aidLen + aid;
|
||
const privHex = document.getElementById('ram-priv').value;
|
||
const privLen = (privHex.length / 2).toString(16).padStart(2, '0');
|
||
dataField += 'C7' + privLen + privHex;
|
||
if (document.getElementById('ram-toolkit-enable').checked) {
|
||
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 ad = (document.getElementById('ram-tk-ad').value || '').replace(/[^0-9a-fA-F]/g, '').padStart(2, '0').slice(0, 2);
|
||
let tkPayload = '';
|
||
if (tkMode === 'ca') {
|
||
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') +
|
||
firstPos.toString(16).padStart(2, '0') + firstId +
|
||
lastPos.toString(16).padStart(2, '0') + lastId +
|
||
channels.toString(16).padStart(2, '0') +
|
||
'01' + msl +
|
||
(tar ? (tar.length / 2).toString(16).padStart(2, '0') + tar : '00') +
|
||
'01' + ad;
|
||
const tkLen = (tkPayload.length / 2).toString(16);
|
||
dataField += 'CA' + tkLen + tkPayload;
|
||
} 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') +
|
||
firstPos.toString(16).padStart(2, '0') + firstId +
|
||
lastPos.toString(16).padStart(2, '0') + lastId +
|
||
channels.toString(16).padStart(2, '0') +
|
||
'01' + msl +
|
||
(tar ? (tar.length / 2).toString(16).padStart(2, '0') + tar : '00');
|
||
const tkLen = (tkPayload.length / 2).toString(16);
|
||
dataField += '80' + tkLen + tkPayload;
|
||
}
|
||
}
|
||
const dataLen = (dataField.length / 2).toString(16).padStart(2, '0');
|
||
apdu = buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dataLen, 16), dataField);
|
||
} else if (cmd === 'install-make-sel') {
|
||
if (!aid) { resultEl.value = 'Error: specify AID'; return; }
|
||
const aidLen = (aid.length / 2).toString(16).padStart(2, '0');
|
||
const dataField = '4F' + aidLen + aid;
|
||
const dataLen = (dataField.length / 2).toString(16).padStart(2, '0');
|
||
apdu = buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dataLen, 16), dataField);
|
||
} else if (cmd === 'install-reg-update' || cmd === 'install-extradition') {
|
||
if (!aid) { resultEl.value = 'Error: specify AID'; return; }
|
||
const aidLen = (aid.length / 2).toString(16).padStart(2, '0');
|
||
const dataField = '4F' + aidLen + aid;
|
||
const dataLen = (dataField.length / 2).toString(16).padStart(2, '0');
|
||
apdu = buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dataLen, 16), dataField);
|
||
} else if (cmd === 'load') {
|
||
if (!ramData) { resultEl.value = 'Error: specify data'; return; }
|
||
const dataLen = (ramData.length / 2).toString(16).padStart(2, '0');
|
||
apdu = buildApdu(0x80, 0xE8, 0x00, blockNum, parseInt(dataLen, 16), ramData);
|
||
} else if (cmd === 'delete') {
|
||
if (!aid) { resultEl.value = 'Error: specify AID'; return; }
|
||
const aidLen = (aid.length / 2).toString(16).padStart(2, '0');
|
||
const dataField = '4F' + aidLen + aid;
|
||
const dataLen = (dataField.length / 2).toString(16).padStart(2, '0');
|
||
apdu = buildApdu(0x80, 0xE4, parseInt(delMode, 16), 0x00, parseInt(dataLen, 16), dataField);
|
||
} 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');
|
||
} else if (cmd === 'get-data') {
|
||
const tag = parseInt(tagHex, 16);
|
||
apdu = buildApdu(0x80, 0xCA, (tag >> 8) & 0xFF, tag & 0xFF, 0x00, null);
|
||
} else if (cmd === 'store-data') {
|
||
if (!ramData) { resultEl.value = 'Error: specify data'; return; }
|
||
const dataLen = (ramData.length / 2).toString(16).padStart(2, '0');
|
||
apdu = buildApdu(0x80, 0xE2, parseInt(enc, 16), blockNum, parseInt(dataLen, 16), ramData);
|
||
} else if (cmd === 'set-status') {
|
||
if (!aid) { resultEl.value = 'Error: specify AID'; return; }
|
||
const aidLen = (aid.length / 2).toString(16).padStart(2, '0');
|
||
const dataField = '4F' + aidLen + aid;
|
||
const dataLen = (dataField.length / 2).toString(16).padStart(2, '0');
|
||
apdu = buildApdu(0x80, 0xF0, parseInt(ssMode, 16), parseInt(ssState, 16), parseInt(dataLen, 16), dataField);
|
||
} else if (cmd === 'ext-auth') {
|
||
if (!ramData) { resultEl.value = 'Error: specify data (host cryptogram)'; return; }
|
||
const dataLen = (ramData.length / 2).toString(16).padStart(2, '0');
|
||
apdu = buildApdu(0x80, 0x82, 0x00, 0x00, parseInt(dataLen, 16), ramData);
|
||
} else if (cmd === 'int-auth') {
|
||
if (!ramData) { resultEl.value = 'Error: specify data (challenge)'; return; }
|
||
const dataLen = (ramData.length / 2).toString(16).padStart(2, '0');
|
||
apdu = buildApdu(0x80, 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('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 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();
|
||
}
|
||
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);
|
||
}
|
||
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);
|
||
}
|
||
function updateKeyPlaceholder(prefix, algNib) {
|
||
const keyLen = {1: 8, 5: 16, 9: 24, 0xD: 8}[algNib] || 8;
|
||
document.getElementById('sp-' + prefix + '-key').placeholder = keyLen + ' bytes key';
|
||
}
|
||
|
||
// ===== 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;
|
||
}
|
||
|
||
function retailMac(data, keyBytes) {
|
||
const bs = 8;
|
||
const padded = zeroPad(data, bs);
|
||
const [k1, k2, k3] = des3Keys(keyBytes);
|
||
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);
|
||
let enc;
|
||
if (keyBytes.length === 8) {
|
||
enc = desEncryptBlock(xored, k1);
|
||
} else {
|
||
enc = des3EncryptBlock(xored, keyBytes);
|
||
}
|
||
if (i + bs === padded.length && keyBytes.length >= 16) {
|
||
enc = desDecryptBlock(enc, k2);
|
||
enc = desEncryptBlock(enc, k1);
|
||
}
|
||
prev = enc;
|
||
}
|
||
return prev;
|
||
}
|
||
|
||
// ===== Secure Packet assembly =====
|
||
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, '').padEnd(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 ciphering = (spi1 & 0x04) !== 0;
|
||
const hasMac = (spi1 & 0x03) === 0x02;
|
||
|
||
const resultEl = document.getElementById('sp-result');
|
||
|
||
let kicKey, kidKey;
|
||
try {
|
||
if (kicKeyHex) {
|
||
kicKey = hexToBytes(kicKeyHex);
|
||
if (kicKey.length !== 8 && kicKey.length !== 16 && kicKey.length !== 24)
|
||
{ resultEl.value = '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);
|
||
if (kidKey.length !== 8 && kidKey.length !== 16 && kidKey.length !== 24)
|
||
{ resultEl.value = '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 pCntr = (8 - (apdu.length % 8)) % 8;
|
||
const pCntrByte = pCntr;
|
||
|
||
const macLen = hasMac ? 8 : 0;
|
||
|
||
const spiOff = 4;
|
||
const kicOff = 6;
|
||
const secDataOff = 17 + macLen;
|
||
|
||
const packetLen = secDataOff + apdu.length + pCntr;
|
||
const packet = new Uint8Array(packetLen);
|
||
|
||
packet[0] = 0x02;
|
||
packet[2] = 0x01;
|
||
packet[4] = spi1;
|
||
packet[5] = spi2;
|
||
packet[6] = parseInt(kicHex, 16);
|
||
packet[7] = parseInt(kidHex, 16);
|
||
packet[8] = parseInt(tarHex.substr(0,2), 16);
|
||
packet[9] = parseInt(tarHex.substr(2,2), 16);
|
||
packet[10] = parseInt(tarHex.substr(4,2), 16);
|
||
packet[11] = parseInt(cntrHex.substr(0,2), 16);
|
||
packet[12] = parseInt(cntrHex.substr(2,2), 16);
|
||
packet[13] = parseInt(cntrHex.substr(4,2), 16);
|
||
packet[14] = parseInt(cntrHex.substr(6,2), 16);
|
||
packet[15] = parseInt(cntrHex.substr(8,2), 16);
|
||
packet[16] = pCntrByte;
|
||
for (let i = 0; i < macLen; i++) packet[17 + i] = 0x00;
|
||
packet.set(apdu, secDataOff);
|
||
for (let i = 0; i < pCntr; i++) packet[secDataOff + apdu.length + i] = padByte;
|
||
|
||
const chl = packetLen - spiOff;
|
||
packet[1] = packetLen - 2;
|
||
packet[3] = chl;
|
||
|
||
if (hasMac && kidKey) {
|
||
const mac = retailMac(packet, kidKey);
|
||
packet.set(mac, 17);
|
||
}
|
||
|
||
if (ciphering && kicKey) {
|
||
const cipherLen = packetLen - kicOff;
|
||
const toEncrypt = packet.subarray(kicOff, packetLen);
|
||
const encrypted = des3CbcEncrypt(toEncrypt, kicKey, new Uint8Array(8));
|
||
packet.set(encrypted, kicOff);
|
||
}
|
||
|
||
resultEl.value = bytesToHex(packet);
|
||
}
|
||
|
||
// ===== BER-TLV =====
|
||
const BER_TAGS = {'c-apdu':'22','immediate':'81','error':'82','chaining':'83'};
|
||
const BER_QUAL = {
|
||
refresh: [
|
||
['00','Full file change notification'],
|
||
['01','File change notification'],
|
||
['02','UICC file change notification'],
|
||
['03','NAA application reset'],
|
||
['04','NAA session reset'],
|
||
['05','UICC reset']
|
||
],
|
||
display: [
|
||
['00','Normal priority'],
|
||
['01','High priority'],
|
||
['80','Clear after delay']
|
||
],
|
||
tone: [
|
||
['00','Monotonous tone'],
|
||
['01','Alternating 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' || type === 'chaining') {
|
||
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 = `<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 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 = `<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)">
|
||
<option value="refresh">REFRESH</option>
|
||
<option value="display">DISPLAY TEXT</option>
|
||
<option value="tone">PLAY TONE</option>
|
||
<option value="custom">Custom</option>
|
||
</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>`;
|
||
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>`;
|
||
} 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 12):</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>`;
|
||
} else {
|
||
fields.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)">`;
|
||
}
|
||
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 === 'c-apdu' || type === 'chaining') {
|
||
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 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:'31',custom:'00'};
|
||
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 = [];
|
||
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('');
|
||
value += '04' + berLenStr(1) + '80';
|
||
} 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) + inner.join('');
|
||
} catch(e) {
|
||
value += '8D' + berLenStr(text.length) + text.split('').map(c=>c.charCodeAt(0).toString(16).padStart(2,'0').toUpperCase()).join('');
|
||
}
|
||
}
|
||
}
|
||
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 += '12' + berLenStr(flist.length/2) + flist;
|
||
} else {
|
||
const hex = (pc.querySelector('.ber-hex').value||'').replace(/[^0-9a-fA-F]/g,'');
|
||
if (hex) value += hex;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
// ===== Response Parser =====
|
||
const SW_MAP = {
|
||
generic: {
|
||
'9000': 'Normal ending of command',
|
||
'61XX': 'Send GET RESPONSE (Le = SW2)',
|
||
'6CXX': 'Repeat command with Le = SW2',
|
||
'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)',
|
||
'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',
|
||
'6A88': 'Referenced data not found',
|
||
'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 — send GET STATUS [next] (P2=42)',
|
||
'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: {
|
||
'9100': 'Normal ending with proactive UICC command pending (SW2 = length of 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': 'LOADED (ELF)',
|
||
'03': 'INSTALLED',
|
||
'07': 'SELECTABLE',
|
||
'0F': 'PERSONALIZED (SD) / SECURED (card)',
|
||
'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, 3); b++) {
|
||
for (let bit = 0; bit < 8; bit++) {
|
||
if (bytes[b] & (0x80 >> bit)) {
|
||
if (b < PRIVILEGE_NAMES.length && bit < PRIVILEGE_NAMES[b].length) {
|
||
names.push(PRIVILEGE_NAMES[b][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 =====
|
||
let pysimBase = 'http://127.0.0.1:8080';
|
||
|
||
function esc(str) {
|
||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||
}
|
||
|
||
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 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);
|
||
}
|
||
|
||
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 || '?') + ']' +
|
||
(data.current_selection.file_type ? ' ' + (({transparent:'TR',linear_fixed:'LF',cyclic:'CY',ber_tlv:'BT',df:'DF'})[data.current_selection.file_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 = 'Connection failed: ' + e.message;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async function pysimConnect() {
|
||
const urlInput = document.getElementById('pysim-url');
|
||
pysimBase = urlInput.value.replace(/\/+$/, '');
|
||
const btn = document.getElementById('pysim-connect-btn');
|
||
btn.disabled = true;
|
||
btn.textContent = 'Connecting...';
|
||
const statusEl = document.getElementById('pysim-status');
|
||
statusEl.innerHTML = '<span class="text-gray-500">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)) {
|
||
statusEl.innerHTML = '<span class="text-red-500">Server version ' + esc(ver.version) + ' is outdated. Please update pysim-otaman-server.</span>';
|
||
btn.textContent = 'Connect';
|
||
btn.disabled = false;
|
||
return;
|
||
}
|
||
if (sv[0] > 1) {
|
||
statusEl.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">No card detected. Insert card and click Equip card button</span>';
|
||
pysimShowConnected(true);
|
||
btn.textContent = 'Connected';
|
||
btn.disabled = false;
|
||
return;
|
||
}
|
||
pysimSetConnected(true);
|
||
pysimShowConnected(true);
|
||
await pysimRefresh();
|
||
pysimLoadCommands();
|
||
pysimFsRefresh();
|
||
btn.textContent = 'Connected';
|
||
} catch (e) {
|
||
pysimSetConnected(false);
|
||
statusEl.innerHTML = '<span class="text-red-500">Connection failed: ' + esc(e.message) + '</span>';
|
||
btn.textContent = 'Connect';
|
||
}
|
||
btn.disabled = false;
|
||
}
|
||
|
||
function pysimDisconnect() {
|
||
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">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">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">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">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">Equipping...</span>';
|
||
pysimSetConnected('spin');
|
||
const result = await pysimRunCmd('equip');
|
||
if (result) {
|
||
await pysimRefresh();
|
||
pysimFsRefresh();
|
||
}
|
||
}
|
||
|
||
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();
|
||
}
|
||
|
||
// ===== pysim sub-tabs =====
|
||
let pysimCmdHistory = [];
|
||
let pysimCmdHistIdx = -1;
|
||
|
||
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'].forEach(s => {
|
||
document.getElementById('pysim-sub-' + s).classList.toggle('hidden', s !== name);
|
||
});
|
||
if (name === 'files') pysimFsRefresh();
|
||
}
|
||
|
||
function pysimCmdKeydown(e) {
|
||
if (e.key === 'Enter') {
|
||
const cmd = e.target.value.trim();
|
||
if (cmd) {
|
||
pysimCmdHistory.push(cmd);
|
||
pysimCmdHistIdx = pysimCmdHistory.length;
|
||
}
|
||
pysimExec();
|
||
} 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 = '';
|
||
}
|
||
}
|
||
}
|
||
|
||
async function pysimLoadCommands() {
|
||
const list = document.getElementById('pysim-cmdlist');
|
||
try {
|
||
const data = await pysimFetch('/api/commands');
|
||
if (Array.isArray(data)) {
|
||
list.innerHTML = data.map(c => '<option value="' + esc(c) + '">').join('');
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
|
||
// ===== 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;
|
||
}
|
||
|
||
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;
|
||
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;
|
||
} 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 = ' '.repeat(depth * 4);
|
||
const isDir = node.isDir;
|
||
const exists = node.exists !== false;
|
||
const color = exists ? (isDir ? 'text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-slate-300') : 'text-red-400 dark:text-red-400';
|
||
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 + '">';
|
||
html += pad;
|
||
if (!exists) {
|
||
html += '✗ ';
|
||
}
|
||
if (isDir) {
|
||
if (exists) {
|
||
const toggle = node.children && node.expanded ? '▼' : '▶';
|
||
html += '<span class="cursor-pointer select-none" onclick="pysimFsToggleDir(\'' + esc(node.name) + '\')">' + toggle + '</span> ';
|
||
html += '<span class="cursor-pointer hover:underline" 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="' + (exists ? 'cursor-pointer hover:underline ml-4' : 'ml-4') + '"' +
|
||
(exists ? ' 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) {
|
||
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">' +
|
||
'<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 pysimFsDecodedMode = false;
|
||
|
||
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);
|
||
});
|
||
}
|
||
|
||
// 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 BER-TLV': 'Сгенерировать BER-TLV',
|
||
'Generate Secure Packet': 'Сгенерировать Secure Packet',
|
||
'Conversion': 'Конвертация',
|
||
'Command': 'Команда',
|
||
'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': 'Привилегии',
|
||
'Byte 1': 'Байт 1',
|
||
'Byte 2': 'Байт 2',
|
||
'SIM/UICC Toolkit parameters': 'Параметры SIM/UICC Toolkit',
|
||
'Toolkit mode': 'Режим Toolkit',
|
||
'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',
|
||
};
|
||
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();
|
||
}
|
||
|
||
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();
|
||
|
||
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> |