Files
otaman/index.html
T

2942 lines
141 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OTAMan</title>
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/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 class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2">SIM OTA с человеческим лицом</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="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">pySim</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">Команда</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')">
Начать с 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">Метод SELECT</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">По FID</option>
<option value="path">По полному пути от MF</option>
<option value="dfname">По DF name / AID</option>
<option value="chain">ADF RFM цепочка</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">Путь (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">FID цепочка (через запятую)</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">Запись №</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">Режим выборки (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">Абсолютный (04)</option>
<option value="06">Следующая (06)</option>
<option value="02">Предыдущая (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">Смещение (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">Данные (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">Размер записи (байт)</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')">
Разрешить редактирование P1/P2
</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">Конвертация</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">Конвертировать</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">Сгенерировать 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">Упаковать в 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">Команда</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')">
Начать с 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">Метод SELECT</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">По FID</option>
<option value="path">По полному пути от MF</option>
<option value="dfname">По DF name / AID</option>
<option value="chain">ADF RFM цепочка</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">Путь (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">FID цепочка (через запятую)</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">Запись №</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">Режим выборки (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">Абсолютный (04)</option>
<option value="06">Следующая (06)</option>
<option value="02">Предыдущая (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">Смещение (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">Данные (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">Размер записи (байт)</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')">
Разрешить редактирование P1/P2
</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">Конвертация</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">Конвертировать</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">Сгенерировать 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">Упаковать в 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">Сгенерировать 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">Упаковать в 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">Команда</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">Привилегии</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">Байт 1</div>
<div class="font-medium text-gray-700 dark:text-slate-300 mb-1">Байт 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
</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">Режим Toolkit</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">Приоритет</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">Таймеры (макс)</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">Длина текста меню</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">Элементов меню</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">Позиция первого меню</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">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">Позиция последнего меню</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">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">Каналы (макс)</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 — нет проверки</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">Домен доступа (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">Данные загрузки (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">Данные (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">Номер блока</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">Шифрование</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">Без шифрования, последний блок</option>
<option value="40">Без шифрования, ещё блоки</option>
<option value="80">С шифрованием, последний блок</option>
<option value="C0">С шифрованием, ещё блоки</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">Режим DELETE</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 (00)</option>
<option value="80">AID и все связанные объекты (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">Режим GET STATUS</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">Приложения и SSD (40)</option>
<option value="20">Executable Load Files (20)</option>
<option value="10">ELF и модули (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">Формат GET STATUS</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">Tag GET DATA</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="">— выберите предустановленный —</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">Тип SET STATUS</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">Приложение или SSD (40)</option>
<option value="60">SD и его приложения (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">Состояние</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">Разблокировать (предыдущее состояние)</option>
<option value="80">Заблокировать (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">Сгенерировать 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">Упаковать в 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">Сгенерировать 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">Команда</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="">— Выберите команду —</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">Данные ответа (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">Декодировать</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 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 items-center gap-2">
<span id="pysim-status-dot" class="inline-block w-3 h-3 rounded-full bg-green-500"></span>
<span class="text-sm font-medium">Connected</span>
<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>
<div id="pysim-status" class="mb-3 text-xs font-mono text-gray-600 dark:text-slate-400"></div>
<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 = 'Ошибка: укажите параметры SELECT'; return; }
resultEl.value = selectApdu;
document.getElementById(mode + '-pack-btn').disabled = false;
return;
}
const op = OPS[cmd];
if (!op) { resultEl.value = 'Ошибка: неизвестная команда'; 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 = 'Ошибка: укажите данные'; return; }
p1 = record; p2 = recMode; p3 = dataHex.length / 2; data = dataHex;
} else if (cmd === 'update-binary') {
if (!dataHex) { resultEl.value = 'Ошибка: укажите данные'; 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 = 'Ошибка: укажите данные'; 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 = 'Ошибка: укажите 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 = 'Ошибка: укажите 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 = 'Ошибка: укажите 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 = 'Ошибка: укажите данные'; 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 = 'Ошибка: укажите 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 = 'Ошибка: укажите данные'; 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 = 'Ошибка: укажите 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 = 'Ошибка: укажите данные (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 = 'Ошибка: укажите данные (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 || 'Ошибка: неизвестная команда';
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 = 'Ошибка: введите значение'; }
else if (!/^\d{15}$/.test(input)) { result = 'Ошибка: IMSI должен быть 15 цифр'; }
else result = encImsi(input);
} else if (type === 'msisdn') {
if (!input) { result = 'Ошибка: введите значение'; }
else result = encMsisdn(input);
} else if (type === 'iccid') {
if (!input) { result = 'Ошибка: введите значение'; }
else result = encIccid(input);
} else if (type === 'spn') {
if (!input) { result = 'Ошибка: введите значение'; }
else result = encodeSpn(input);
} else if (type === 'plmn') {
if (!mcc || !mnc) { result = 'Ошибка: укажите MCC и MNC'; }
else if (!/^\d{3}$/.test(mcc)) { result = 'Ошибка: MCC должен быть 3 цифры'; }
else if (!/^\d{2,3}$/.test(mnc)) { result = 'Ошибка: MNC должен быть 2-3 цифры'; }
else result = encPlmn(mcc, mnc);
} else if (type === 'plmnwact') {
if (!mcc || !mnc) { result = 'Ошибка: укажите MCC и MNC'; }
else if (!/^\d{3}$/.test(mcc)) { result = 'Ошибка: MCC должен быть 3 цифры'; }
else if (!/^\d{2,3}$/.test(mnc)) { result = 'Ошибка: MNC должен быть 2-3 цифры'; }
else result = encPlmn(mcc, mnc) + act.toUpperCase();
} else if (type === 'nibswap') {
if (!input) { result = 'Ошибка: введите значение'; }
else if (input.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(input)) { result = 'Ошибка: введите чётное количество hex-символов'; }
else result = swapNibbles(input);
}
} catch (e) {
result = 'Ошибка: ' + 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 = 'Ошибка: укажите 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 = 'Ошибка: KIc key must be 8, 16, or 24 bytes'; return; }
}
} catch (e) { resultEl.value = 'Ошибка KIc key: ' + e.message; return; }
try {
if (kidKeyHex) {
kidKey = hexToBytes(kidKeyHex);
if (kidKey.length !== 8 && kidKey.length !== 16 && kidKey.length !== 24)
{ resultEl.value = 'Ошибка: KID key must be 8, 16, or 24 bytes'; return; }
}
} catch (e) { resultEl.value = 'Ошибка KID key: ' + e.message; return; }
if (ciphering && !kicKey) { resultEl.value = 'Ошибка: ciphering requires KIc key'; return; }
if (hasMac && !kidKey) { resultEl.value = 'Ошибка: 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
async function pysimFetch(path, body) {
const url = pysimBase + path;
const opts = { method: body ? 'POST' : 'GET', headers: { 'Content-Type': 'application/json' } };
if (body) opts.body = JSON.stringify(body);
const res = await fetch(url, opts);
return await res.json();
}
function pysimSetDot(ok) {
const dot = document.getElementById('pysim-status-dot');
dot.className = 'inline-block w-3 h-3 rounded-full ' + (ok ? 'bg-green-500' : 'bg-red-500');
}
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);
}
async function pysimRefresh() {
const statusEl = document.getElementById('pysim-status');
try {
const data = await pysimFetch('/api/status');
pysimSetDot(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) {
pysimSetDot(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>';
try {
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'))) {
pysimSetDot(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;
}
pysimSetDot(true);
pysimShowConnected(true);
await pysimRefresh();
pysimLoadCommands();
pysimFsRefresh();
btn.textContent = 'Connected';
} catch (e) {
pysimSetDot(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 = '';
pysimSetDot(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'))) {
pysimSetDot(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) {
pysimSetDot(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>';
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>';
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>';
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 = '&nbsp;'.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));
});
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();
// 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>