Files
otaman/index.html
T
2026-08-05 02:44:50 +03:00

1889 lines
97 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">
<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-10">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-heading">OTAMan</h1>
<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 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>
</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 (4 hex)</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="A153,4953">
</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="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>
<option value="nibswap">Nibble swap</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 (4 hex)</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="A153,4953">
</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="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>
<option value="nibswap">Nibble swap</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 (6 hex)</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">Связанные объекты (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-tag-row" class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Tag (2 байта hex)</label>
<input id="ram-tag" class="font-mono 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 (6 hex)</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 (10 hex)</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="16B key (32 hex)"></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="16B key (32 hex)"></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>
<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-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') {
apdu = buildApdu(0x80, 0xF2, parseInt(gsMode, 16), 0x00, 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 + 'B key (' + (keyLen * 2) + ' hex)';
}
// ===== 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;
}
// Init
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();
</script>
</body>
</html>