1918 lines
99 KiB
HTML
1918 lines
99 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>OTAMan</title>
|
||
<link rel="stylesheet" href="style.css">
|
||
<link rel="icon" type="image/png" href="sim.png">
|
||
<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>
|
||
<div class="flex items-center gap-2">
|
||
<a href="https://github.com/anttro/otaman" target="_blank" class="text-xs text-gray-400 hover:text-gray-600 dark:text-slate-500 dark:hover:text-slate-300">github</a>
|
||
<button onclick="toggleTheme()" id="theme-btn" class="px-2 py-1 text-sm rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700">🌙</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex gap-1 mb-4 border-b border-gray-300 dark:border-slate-600">
|
||
<button class="tab-btn active px-4 py-2 text-sm rounded-t bg-blue-600 dark:bg-blue-500 text-white dark:text-white" data-tab="sim">SIM RFM</button>
|
||
<button class="tab-btn px-4 py-2 text-sm rounded-t bg-gray-200 dark:bg-slate-700 hover:bg-gray-300 dark:hover:bg-slate-600 text-gray-700 dark:text-slate-300" data-tab="usim">USIM RFM</button>
|
||
<button class="tab-btn px-4 py-2 text-sm rounded-t bg-gray-200 dark:bg-slate-700 hover:bg-gray-300 dark:hover:bg-slate-600 text-gray-700 dark:text-slate-300" data-tab="ber">BER-TLV</button>
|
||
<button class="tab-btn px-4 py-2 text-sm rounded-t bg-gray-200 dark:bg-slate-700 hover:bg-gray-300 dark:hover:bg-slate-600 text-gray-700 dark:text-slate-300" data-tab="ram">RAM</button>
|
||
<button class="tab-btn px-4 py-2 text-sm rounded-t bg-gray-200 dark:bg-slate-700 hover:bg-gray-300 dark:hover:bg-slate-600 text-gray-700 dark:text-slate-300" data-tab="sp">Secured Packet</button>
|
||
</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">AID и все связанные объекты (80)</option>
|
||
</select>
|
||
</div>
|
||
<div id="ram-gs-mode-row" class="mb-3">
|
||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Режим GET STATUS</label>
|
||
<select id="ram-gs-mode" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800">
|
||
<option value="80">ISD (80)</option>
|
||
<option value="40">Приложения и SSD (40)</option>
|
||
<option value="20">Executable Load Files (20)</option>
|
||
<option value="10">ELF и модули (10)</option>
|
||
</select>
|
||
</div>
|
||
<div id="ram-gs-p2-row" class="mb-3">
|
||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Формат GET STATUS</label>
|
||
<select id="ram-gs-p2" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800">
|
||
<option value="40">40 — First/all, GP TLV format</option>
|
||
<option value="42">42 — Next, GP TLV format</option>
|
||
<option value="00">00 — First/all, old format (deprecated)</option>
|
||
<option value="02">02 — Next, old format (deprecated)</option>
|
||
</select>
|
||
</div>
|
||
<div id="ram-tag-row" class="mb-3">
|
||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Tag GET DATA</label>
|
||
<select id="ram-gd-tag-sel" onchange="document.getElementById('ram-tag').value=this.value" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800 mb-1.5">
|
||
<option value="">— выберите предустановленный —</option>
|
||
<option value="42">42 — Issuer Identification Number (IIN)</option>
|
||
<option value="45">45 — Card Image Number (CIN)</option>
|
||
<option value="66">66 — Card Data / SD Management Data</option>
|
||
<option value="67">67 — Card Capability Information</option>
|
||
<option value="E0">E0 — Key Information Template</option>
|
||
<option value="D3">D3 — Current Security Level</option>
|
||
<option value="2F00">2F00 — List of Applications</option>
|
||
<option value="FF21">FF21 — Extended Card Resources Info</option>
|
||
<option value="5F50">5F50 — SD Manager URL</option>
|
||
<option value="C1">C1 — Sequence Counter (SCP02/03)</option>
|
||
<option value="C2">C2 — Confirmation Counter</option>
|
||
<option value="7F21">7F21 — Certificate (SD public key)</option>
|
||
<option value="5031">5031 — Certificate info (EF.OD)</option>
|
||
</select>
|
||
<input id="ram-tag" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800" placeholder="5F50" maxlength="4" value="5F50">
|
||
</div>
|
||
<div id="ram-ss-mode-row" class="mb-3">
|
||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Тип SET STATUS</label>
|
||
<select id="ram-ss-mode" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800">
|
||
<option value="80">ISD (80)</option>
|
||
<option value="40">Приложение или SSD (40)</option>
|
||
<option value="60">SD и его приложения (60)</option>
|
||
</select>
|
||
</div>
|
||
<div id="ram-ss-state-row" class="mb-3">
|
||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Состояние</label>
|
||
<select id="ram-ss-state" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800">
|
||
<option value="00">Разблокировать (предыдущее состояние)</option>
|
||
<option value="80">Заблокировать (LOCKED)</option>
|
||
</select>
|
||
</div>
|
||
<button onclick="genRam()" class="mb-3 px-5 py-2.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700">Сгенерировать APDU</button>
|
||
<textarea id="ram-result" rows="3" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 bg-gray-100 dark:bg-slate-800"></textarea>
|
||
<button onclick="packToSp('ram-result')" id="ram-pack-btn" disabled class="mb-3 px-5 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700 disabled:opacity-40 disabled:cursor-not-allowed">Упаковать в Secured packet</button>
|
||
</div>
|
||
|
||
<div id="tab-sp" class="tab-content hidden">
|
||
<div class="mb-3">
|
||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">SPI1 (Security Parameter Indicator)</label>
|
||
<div class="flex gap-2 items-center">
|
||
<select id="sp-spi1" onchange="updateSp()" class="flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800">
|
||
<option value="00">00 — No security</option>
|
||
<option value="01">01 — RC only</option>
|
||
<option value="02">02 — CC/MAC only</option>
|
||
<option value="06" selected>06 — CC/MAC + Ciphering</option>
|
||
<option value="12">12 — CC/MAC + Counter (available)</option>
|
||
<option value="16">16 — CC/MAC + Ciphering + Counter (available)</option>
|
||
<option value="22">22 — CC/MAC + Counter (check higher)</option>
|
||
<option value="26">26 — CC/MAC + Ciphering + Counter (check higher)</option>
|
||
<option value="32">32 — CC/MAC + Counter (check +1)</option>
|
||
<option value="36">36 — CC/MAC + Ciphering + Counter (check +1)</option>
|
||
</select>
|
||
<input id="sp-spi1-hex" readonly class="w-16 font-mono border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-2 bg-gray-100 dark:bg-slate-800 text-center" value="06">
|
||
</div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">SPI2 (PoR settings)</label>
|
||
<div class="flex gap-2 items-center">
|
||
<select id="sp-spi2" onchange="updateSp()" class="flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800">
|
||
<option value="00" selected>00 — No PoR, no security</option>
|
||
<option value="01">01 — PoR required, no security</option>
|
||
<option value="05">05 — PoR required, RC</option>
|
||
<option value="09">09 — PoR required, CC</option>
|
||
<option value="0D">0D — PoR required, DS</option>
|
||
<option value="11">11 — PoR required, no security, ciphered</option>
|
||
<option value="02">02 — PoR on error, no security</option>
|
||
<option value="06">06 — PoR on error, RC</option>
|
||
</select>
|
||
<select id="sp-spi2-sm" onchange="updateSp()" class="border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800">
|
||
<option value="0">DELIVER-REPORT</option>
|
||
<option value="1">SUBMIT SM</option>
|
||
</select>
|
||
<input id="sp-spi2-hex" readonly class="w-16 font-mono border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-2 bg-gray-100 dark:bg-slate-800 text-center" value="00">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">KIc (Ciphering Key Identifier)</label>
|
||
<div class="flex gap-2 items-center">
|
||
<select id="sp-kic-idx" onchange="updateSpKic()" class="border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800">
|
||
<option value="0">Implicit (0)</option>
|
||
<option value="1" selected>1</option>
|
||
<option value="2">2</option>
|
||
<option value="3">3</option>
|
||
<option value="4">4</option>
|
||
<option value="5">5</option>
|
||
<option value="6">6</option>
|
||
<option value="7">7</option>
|
||
<option value="8">8</option>
|
||
<option value="9">9</option>
|
||
<option value="A">A</option>
|
||
<option value="B">B</option>
|
||
<option value="C">C</option>
|
||
<option value="D">D</option>
|
||
<option value="E">E</option>
|
||
<option value="F">F</option>
|
||
</select>
|
||
<select id="sp-kic-alg" onchange="updateSpKic()" class="flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800">
|
||
<option value="01">01 — DES-CBC (8B key)</option>
|
||
<option value="05" selected>05 — 3DES 2-key outer-CBC (16B key)</option>
|
||
<option value="09">09 — 3DES 3-key outer-CBC (24B key)</option>
|
||
<option value="0D">0D — DES-ECB (8B key)</option>
|
||
</select>
|
||
<input id="sp-kic-hex" readonly class="w-12 font-mono border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-2 bg-gray-100 dark:bg-slate-800 text-center" value="15">
|
||
</div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">KID (MAC Key Identifier)</label>
|
||
<div class="flex gap-2 items-center">
|
||
<select id="sp-kid-idx" onchange="updateSpKid()" class="border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800">
|
||
<option value="0">Implicit (0)</option>
|
||
<option value="1" selected>1</option>
|
||
<option value="2">2</option>
|
||
<option value="3">3</option>
|
||
<option value="4">4</option>
|
||
<option value="5">5</option>
|
||
<option value="6">6</option>
|
||
<option value="7">7</option>
|
||
<option value="8">8</option>
|
||
<option value="9">9</option>
|
||
<option value="A">A</option>
|
||
<option value="B">B</option>
|
||
<option value="C">C</option>
|
||
<option value="D">D</option>
|
||
<option value="E">E</option>
|
||
<option value="F">F</option>
|
||
</select>
|
||
<select id="sp-kid-alg" onchange="updateSpKid()" class="flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800">
|
||
<option value="01">01 — DES-CBC (8B key)</option>
|
||
<option value="05" selected>05 — 3DES 2-key outer-CBC (16B key)</option>
|
||
<option value="09">09 — 3DES 3-key outer-CBC (24B key)</option>
|
||
</select>
|
||
<input id="sp-kid-hex" readonly class="w-12 font-mono border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-2 bg-gray-100 dark:bg-slate-800 text-center" value="15">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex gap-2 mb-3">
|
||
<div class="flex-1">
|
||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">TAR (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') {
|
||
const gsP2 = document.getElementById('ram-gs-p2').value;
|
||
apdu = buildApdu(0x80, 0xF2, parseInt(gsMode, 16), parseInt(gsP2, 16), 0x02, '4F00');
|
||
} else if (cmd === 'get-data') {
|
||
const tag = parseInt(tagHex, 16);
|
||
apdu = buildApdu(0x80, 0xCA, (tag >> 8) & 0xFF, tag & 0xFF, 0x00, null);
|
||
} else if (cmd === 'store-data') {
|
||
if (!ramData) { resultEl.value = 'Ошибка: укажите данные'; return; }
|
||
const dataLen = (ramData.length / 2).toString(16).padStart(2, '0');
|
||
apdu = buildApdu(0x80, 0xE2, parseInt(enc, 16), blockNum, parseInt(dataLen, 16), ramData);
|
||
} else if (cmd === 'set-status') {
|
||
if (!aid) { resultEl.value = 'Ошибка: укажите AID'; return; }
|
||
const aidLen = (aid.length / 2).toString(16).padStart(2, '0');
|
||
const dataField = '4F' + aidLen + aid;
|
||
const dataLen = (dataField.length / 2).toString(16).padStart(2, '0');
|
||
apdu = buildApdu(0x80, 0xF0, parseInt(ssMode, 16), parseInt(ssState, 16), parseInt(dataLen, 16), dataField);
|
||
} else if (cmd === 'ext-auth') {
|
||
if (!ramData) { resultEl.value = 'Ошибка: укажите данные (host cryptogram)'; return; }
|
||
const dataLen = (ramData.length / 2).toString(16).padStart(2, '0');
|
||
apdu = buildApdu(0x80, 0x82, 0x00, 0x00, parseInt(dataLen, 16), ramData);
|
||
} else if (cmd === 'int-auth') {
|
||
if (!ramData) { resultEl.value = 'Ошибка: укажите данные (challenge)'; return; }
|
||
const dataLen = (ramData.length / 2).toString(16).padStart(2, '0');
|
||
apdu = buildApdu(0x80, 0x88, 0x00, 0x00, parseInt(dataLen, 16), ramData) + '00';
|
||
}
|
||
|
||
resultEl.value = apdu || 'Ошибка: неизвестная команда';
|
||
if (apdu) document.getElementById('ram-pack-btn').disabled = false;
|
||
}
|
||
|
||
function updateConvFields() {
|
||
const type = document.getElementById('conv-type').value;
|
||
const isPlmn = type === 'plmn' || type === 'plmnwact';
|
||
document.getElementById('conv-input').style.display = isPlmn ? 'none' : '';
|
||
document.getElementById('conv-plmn-fields').style.display = isPlmn ? '' : 'none';
|
||
document.getElementById('conv-act-row').style.display = type === 'plmnwact' ? '' : 'none';
|
||
}
|
||
|
||
function encPlmn(mcc, mnc) {
|
||
mcc = mcc.padEnd(3, '0').slice(0, 3);
|
||
mnc = mnc.padEnd(3, 'F').slice(0, 3);
|
||
const d = mcc + mnc;
|
||
const b1 = d[1] + d[0];
|
||
const b2 = d[5] + d[2];
|
||
const b3 = d[4] + d[3];
|
||
return (b1 + b2 + b3).toUpperCase();
|
||
}
|
||
|
||
function genConvert() {
|
||
const type = document.getElementById('conv-type').value;
|
||
const input = document.getElementById('conv-input').value.trim();
|
||
const mcc = document.getElementById('conv-mcc').value.trim();
|
||
const mnc = document.getElementById('conv-mnc').value.trim();
|
||
const act = document.getElementById('conv-act').value;
|
||
|
||
let result = '';
|
||
try {
|
||
if (type === 'imsi') {
|
||
if (!input) { result = 'Ошибка: введите значение'; }
|
||
else if (!/^\d{15}$/.test(input)) { result = 'Ошибка: IMSI должен быть 15 цифр'; }
|
||
else result = encImsi(input);
|
||
} else if (type === 'msisdn') {
|
||
if (!input) { result = 'Ошибка: введите значение'; }
|
||
else result = encMsisdn(input);
|
||
} else if (type === 'iccid') {
|
||
if (!input) { result = 'Ошибка: введите значение'; }
|
||
else result = encIccid(input);
|
||
} else if (type === 'spn') {
|
||
if (!input) { result = 'Ошибка: введите значение'; }
|
||
else result = encodeSpn(input);
|
||
} else if (type === 'plmn') {
|
||
if (!mcc || !mnc) { result = 'Ошибка: укажите MCC и MNC'; }
|
||
else if (!/^\d{3}$/.test(mcc)) { result = 'Ошибка: MCC должен быть 3 цифры'; }
|
||
else if (!/^\d{2,3}$/.test(mnc)) { result = 'Ошибка: MNC должен быть 2-3 цифры'; }
|
||
else result = encPlmn(mcc, mnc);
|
||
} else if (type === 'plmnwact') {
|
||
if (!mcc || !mnc) { result = 'Ошибка: укажите MCC и MNC'; }
|
||
else if (!/^\d{3}$/.test(mcc)) { result = 'Ошибка: MCC должен быть 3 цифры'; }
|
||
else if (!/^\d{2,3}$/.test(mnc)) { result = 'Ошибка: MNC должен быть 2-3 цифры'; }
|
||
else result = encPlmn(mcc, mnc) + act.toUpperCase();
|
||
} else if (type === 'nibswap') {
|
||
if (!input) { result = 'Ошибка: введите значение'; }
|
||
else if (input.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(input)) { result = 'Ошибка: введите чётное количество hex-символов'; }
|
||
else result = swapNibbles(input);
|
||
}
|
||
} catch (e) {
|
||
result = 'Ошибка: ' + e.message;
|
||
}
|
||
document.getElementById('conv-result').value = result;
|
||
}
|
||
|
||
// ===== Secured Packet UI =====
|
||
function packToSp(sourceId) {
|
||
const val = document.getElementById(sourceId).value;
|
||
if (!val) return;
|
||
switchTab('sp');
|
||
document.getElementById('sp-apdu').value = val;
|
||
}
|
||
function spCntrAdjust(delta) {
|
||
const el = document.getElementById('sp-cntr');
|
||
let val = parseInt(el.value.replace(/[^0-9a-fA-F]/g, ''), 16) || 0;
|
||
val = Math.max(0, val + delta);
|
||
el.value = val.toString(16).padStart(10, '0').toUpperCase();
|
||
}
|
||
function updateSp() {
|
||
document.getElementById('sp-spi1-hex').value = document.getElementById('sp-spi1').value;
|
||
const spi2Base = parseInt(document.getElementById('sp-spi2').value, 16);
|
||
const sm = parseInt(document.getElementById('sp-spi2-sm').value, 16);
|
||
const spi2Byte = spi2Base | (sm << 5);
|
||
document.getElementById('sp-spi2-hex').value = spi2Byte.toString(16).padStart(2, '0').toUpperCase();
|
||
}
|
||
function updateSpKic() {
|
||
const idx = parseInt(document.getElementById('sp-kic-idx').value, 16);
|
||
const alg = parseInt(document.getElementById('sp-kic-alg').value, 16);
|
||
const byte = (idx << 4) | alg;
|
||
document.getElementById('sp-kic-hex').value = byte.toString(16).padStart(2, '0').toUpperCase();
|
||
updateKeyPlaceholder('kic', alg & 0x0F);
|
||
}
|
||
function updateSpKid() {
|
||
const idx = parseInt(document.getElementById('sp-kid-idx').value, 16);
|
||
const alg = parseInt(document.getElementById('sp-kid-alg').value, 16);
|
||
const byte = (idx << 4) | alg;
|
||
document.getElementById('sp-kid-hex').value = byte.toString(16).padStart(2, '0').toUpperCase();
|
||
updateKeyPlaceholder('kid', alg & 0x0F);
|
||
}
|
||
function updateKeyPlaceholder(prefix, algNib) {
|
||
const keyLen = {1: 8, 5: 16, 9: 24, 0xD: 8}[algNib] || 8;
|
||
document.getElementById('sp-' + prefix + '-key').placeholder = keyLen + '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> |