TS 102 226: Expanded Script compliance with Error Action, Script Chaining, Expanded Response
Server: - ExpandedRemoteResponse parser for per-command results (TS 102 226 §5.2.2) - _decode_por() tries expanded parsing first, fallback to CompactRemoteResp - response_type indicator: 'expanded'/'compact'/'none' + per-command details Frontend: - Error Action TLV (tag 82): structured UI with Action Type, SW pattern builder, recovery command, retry count - Script Chaining TLV (tag 83): First/Intermediary/Last flags, Script ID with auto-increment hints, Additional Data - Error Action replaces old 'action indicator' dropdown (breaking change) - genErrorActionValue()/genScriptChainingValue() encoding functions - updateSwMaskDisplay() for visual SW pattern builder Documentation: - help.html/help-ru.html: Error Action, Script Chaining, Expanded Response sections Tests: - TestExpandedRemoteResponse: construct parsing, error handling, chaining context - 49 Python + 14 Node tests green
This commit is contained in:
+30
-2
@@ -102,12 +102,40 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">C-APDU</td><td class="py-1 px-2 font-mono">22</td><td class="py-1 px-2">APDU</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">C-APDU</td><td class="py-1 px-2 font-mono">22</td><td class="py-1 px-2">APDU</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Immediate Action</td><td class="py-1 px-2 font-mono">81</td><td class="py-1 px-2">Проактивная команда или action indicator</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Immediate Action</td><td class="py-1 px-2 font-mono">81</td><td class="py-1 px-2">Проактивная команда или action indicator</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Error Action</td><td class="py-1 px-2 font-mono">82</td><td class="py-1 px-2">Проактивная команда при ошибке</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Error Action</td><td class="py-1 px-2 font-mono">82</td><td class="py-1 px-2">Условное восстановление при ошибках с проактивной командой</td></tr>
|
||||||
<tr><td class="py-1 px-2">Script Chaining</td><td class="py-1 px-2 font-mono">83</td><td class="py-1 px-2">Данные для многопакетных скриптов</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Script Chaining</td><td class="py-1 px-2 font-mono">83</td><td class="py-1 px-2">Многопакетное выполнение скрипта с флагами First/Intermediary/Last</td></tr>
|
||||||
|
<tr><td class="py-1 px-2">Response Type</td><td class="py-1 px-2 font-mono">-</td><td class="py-1 px-2">Индикатор типа ответа: expanded/compact/none</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p class="text-sm mb-3">Сборщик Immediate Action предлагает action indicator (<code class="font-mono text-sm">81</code>/<code class="font-mono text-sm">82</code>), структурированный сборщик проактивных команд (REFRESH, DISPLAY TEXT, PLAY TONE с авто-генерацией COMPREHENSION-TLV), или ручной hex-ввод.</p>
|
<p class="text-sm mb-3">Сборщик Immediate Action предлагает action indicator (<code class="font-mono text-sm">81</code>/<code class="font-mono text-sm">82</code>), структурированный сборщик проактивных команд (REFRESH, DISPLAY TEXT, PLAY TONE с авто-генерацией COMPREHENSION-TLV), или ручной hex-ввод.</p>
|
||||||
|
|
||||||
|
<h4 id="ber-error-action" class="font-medium mb-2 text-base">Error Action TLV (Tag 82)</h4>
|
||||||
|
<p class="text-sm mb-2">Условное восстановление при ошибках для graceful rollback:</p>
|
||||||
|
<ul class="text-sm list-disc pl-5 mb-2">
|
||||||
|
<li><strong>Тип действия:</strong> Немедленное (00), отложенное (01) или условное (02) выполнение</li>
|
||||||
|
<li><strong>Условия запуска:</strong> Сопоставление SW (любая ошибка, конкретный SW, диапазон SW, конкретная команда)</li>
|
||||||
|
<li><strong>Действие восстановления:</strong> Проактивная команда (DISPLAY TEXT, PLAY TONE, REFRESH) для уведомления или отката</li>
|
||||||
|
<li><strong>Количество попыток:</strong> Число повторных попыток до отказа (0=без повтора)</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4 id="ber-script-chaining" class="font-medium mb-2 text-base">Script Chaining TLV (Tag 83)</h4>
|
||||||
|
<p class="text-sm mb-2">Многопакетное выполнение скрипта с сохранением контекста:</p>
|
||||||
|
<ul class="text-sm list-disc pl-5 mb-2">
|
||||||
|
<li><strong>Флаги цепочки:</strong> Позиция First/Intermediary/Last Script (бит 0/1/2)</li>
|
||||||
|
<li><strong>Идентификатор скрипта:</strong> Корреляционный идентификатор между пакетами (1-4 байта hex, авто-инкремент подсказки)</li>
|
||||||
|
<li><strong>Дополнительные данные:</strong> Расширенная информация цепочки (опционально hex)</li>
|
||||||
|
<li><strong>Сохранение контекста:</strong> UICC сохраняет состояние безопасности/транзакции между пакетами</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4 id="expanded-response" class="font-medium mb-2 text-base">Expanded Remote Response (TS 102 226 §5.2.2)</h4>
|
||||||
|
<p class="text-sm mb-2">Результаты по каждой команде с деталями ошибок и контекстом цепочки:</p>
|
||||||
|
<ul class="text-sm list-disc pl-5">
|
||||||
|
<li>Номер команды, статус слова, данные ответа для каждой команды</li>
|
||||||
|
<li>Код ошибки и информация о ней для неудачных команд (подсвечено красным)</li>
|
||||||
|
<li>Идентификатор скрипта и позиция для корреляции цепочки (ID, FIRST, LAST)</li>
|
||||||
|
<li>Индикатор типа ответа: 'expanded' vs 'compact' vs 'none'</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<h3 id="ram-gp" class="text-lg font-medium mb-2">2.4 RAM/GP</h3>
|
<h3 id="ram-gp" class="text-lg font-medium mb-2">2.4 RAM/GP</h3>
|
||||||
<p class="mb-2">CLA = <code class="font-mono text-sm">80</code> (GlobalPlatform Card Specification v2.3.1). Команды удалённого управления приложениями.</p>
|
<p class="mb-2">CLA = <code class="font-mono text-sm">80</code> (GlobalPlatform Card Specification v2.3.1). Команды удалённого управления приложениями.</p>
|
||||||
<table class="w-full text-sm mb-3 border-collapse">
|
<table class="w-full text-sm mb-3 border-collapse">
|
||||||
|
|||||||
+30
-2
@@ -102,12 +102,40 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">C-APDU</td><td class="py-1 px-2 font-mono">22</td><td class="py-1 px-2">Raw APDU hex</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">C-APDU</td><td class="py-1 px-2 font-mono">22</td><td class="py-1 px-2">Raw APDU hex</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Immediate Action</td><td class="py-1 px-2 font-mono">81</td><td class="py-1 px-2">Proactive command or action indicator</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Immediate Action</td><td class="py-1 px-2 font-mono">81</td><td class="py-1 px-2">Proactive command or action indicator</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Error Action</td><td class="py-1 px-2 font-mono">82</td><td class="py-1 px-2">Proactive command on error</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Error Action</td><td class="py-1 px-2 font-mono">82</td><td class="py-1 px-2">Conditional error recovery with proactive command fallback</td></tr>
|
||||||
<tr><td class="py-1 px-2">Script Chaining</td><td class="py-1 px-2 font-mono">83</td><td class="py-1 px-2">Chaining data for multi-packet scripts</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Script Chaining</td><td class="py-1 px-2 font-mono">83</td><td class="py-1 px-2">Multi-packet script execution with First/Intermediary/Last flags</td></tr>
|
||||||
|
<tr><td class="py-1 px-2">Response Type</td><td class="py-1 px-2 font-mono">-</td><td class="py-1 px-2">Expanded/Compact/None response parsing indicator</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p class="text-sm mb-3">The Immediate Action builder offers an action indicator (<code class="font-mono text-sm">81</code>/<code class="font-mono text-sm">82</code>), a structured proactive command builder (REFRESH, DISPLAY TEXT, PLAY TONE with auto-generated COMPREHENSION-TLV objects), or a freeform hex input.</p>
|
<p class="text-sm mb-3">The Immediate Action builder offers an action indicator (<code class="font-mono text-sm">81</code>/<code class="font-mono text-sm">82</code>), a structured proactive command builder (REFRESH, DISPLAY TEXT, PLAY TONE with auto-generated COMPREHENSION-TLV objects), or a freeform hex input.</p>
|
||||||
|
|
||||||
|
<h4 id="ber-error-action" class="font-medium mb-2 text-base">Error Action TLV (Tag 82)</h4>
|
||||||
|
<p class="text-sm mb-2">Conditional error recovery for graceful rollback:</p>
|
||||||
|
<ul class="text-sm list-disc pl-5 mb-2">
|
||||||
|
<li><strong>Action Type:</strong> Immediate (00), delayed (01), or conditional (02) execution</li>
|
||||||
|
<li><strong>Trigger Conditions:</strong> SW pattern matching (any error, specific SW, SW range, specific command fails)</li>
|
||||||
|
<li><strong>Recovery Action:</strong> Proactive command (DISPLAY TEXT, PLAY TONE, REFRESH) to notify user or fallback</li>
|
||||||
|
<li><strong>Retry Count:</strong> Number of retry attempts before giving up (0=no retry)</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4 id="ber-script-chaining" class="font-medium mb-2 text-base">Script Chaining TLV (Tag 83)</h4>
|
||||||
|
<p class="text-sm mb-2">Multi-packet script execution with context preservation:</p>
|
||||||
|
<ul class="text-sm list-disc pl-5 mb-2">
|
||||||
|
<li><strong>Chaining Flags:</strong> First/Intermediary/Last Script position (bit 0/1/2)</li>
|
||||||
|
<li><strong>Script ID:</strong> Correlation identifier across packets (1-4 bytes hex, auto-increment hints provided)</li>
|
||||||
|
<li><strong>Additional Data:</strong> Extended chaining information (optional hex)</li>
|
||||||
|
<li><strong>Context Preservation:</strong> UICC keeps security/transaction state open across chained scripts</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4 id="expanded-response" class="font-medium mb-2 text-base">Expanded Remote Response (TS 102 226 §5.2.2)</h4>
|
||||||
|
<p class="text-sm mb-2">Per-command results with error details and chaining context:</p>
|
||||||
|
<ul class="text-sm list-disc pl-5">
|
||||||
|
<li>Command number, status word, response data for each command</li>
|
||||||
|
<li>Error code and error info for failed commands (highlighted in red)</li>
|
||||||
|
<li>Script ID and position for chained script correlation (ID, FIRST, LAST)</li>
|
||||||
|
<li>Response type indicator: 'expanded' vs 'compact' vs 'none'</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<h3 id="ram-gp" class="text-lg font-medium mb-2">2.4 RAM/GP</h3>
|
<h3 id="ram-gp" class="text-lg font-medium mb-2">2.4 RAM/GP</h3>
|
||||||
<p class="mb-2">CLA = <code class="font-mono text-sm">80</code> (GlobalPlatform Card Specification v2.3.1). Remote Application Management commands for card content management.</p>
|
<p class="mb-2">CLA = <code class="font-mono text-sm">80</code> (GlobalPlatform Card Specification v2.3.1). Remote Application Management commands for card content management.</p>
|
||||||
<table class="w-full text-sm mb-3 border-collapse">
|
<table class="w-full text-sm mb-3 border-collapse">
|
||||||
|
|||||||
+274
-2
@@ -2045,8 +2045,10 @@ function onBerTypeChange(sel) {
|
|||||||
const row = sel.closest('.ber-row');
|
const row = sel.closest('.ber-row');
|
||||||
const body = row.querySelector('.ber-body');
|
const body = row.querySelector('.ber-body');
|
||||||
const type = sel.value;
|
const type = sel.value;
|
||||||
if (type === 'c-apdu' || type === 'chaining') {
|
if (type === 'c-apdu') {
|
||||||
body.innerHTML = `<input class="ber-hex w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-1 py-1 dark:bg-slate-700 dark:text-slate-300" placeholder="hex" oninput="updateBerRow(this)">`;
|
body.innerHTML = `<input class="ber-hex w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-1 py-1 dark:bg-slate-700 dark:text-slate-300" placeholder="hex" oninput="updateBerRow(this)">`;
|
||||||
|
} else if (type === 'chaining') {
|
||||||
|
body.innerHTML = buildScriptChainingRow(row.dataset.berIdx);
|
||||||
} else {
|
} else {
|
||||||
body.innerHTML = `<div class="flex gap-1 mb-1 flex-wrap">
|
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)">
|
<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)">
|
||||||
@@ -2061,14 +2063,88 @@ function onBerTypeChange(sel) {
|
|||||||
updateBerRow(sel);
|
updateBerRow(sel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildErrorActionRow(rowIdx) {
|
||||||
|
return `
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<span class="text-xs text-gray-600 dark:text-slate-400">Action Type:</span>
|
||||||
|
<select class="error-action-type text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-700 dark:text-slate-300">
|
||||||
|
<option value="00">Immediate execution</option>
|
||||||
|
<option value="01">Delayed execution</option>
|
||||||
|
<option value="02">Conditional execution</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border border-gray-300 dark:border-slate-600 rounded p-2">
|
||||||
|
<div class="text-xs mb-2 font-medium text-gray-700 dark:text-slate-300">Trigger Conditions:</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2 mb-2 items-center">
|
||||||
|
<span class="text-xs text-gray-600 dark:text-slate-400">When:</span>
|
||||||
|
<select class="error-trigger-type text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-700 dark:text-slate-300">
|
||||||
|
<option value="any-sw">Any error</option>
|
||||||
|
<option value="specific-sw">Specific SW</option>
|
||||||
|
<option value="sw-range">SW range</option>
|
||||||
|
<option value="cmd-number">Specific command fails</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<span class="text-xs text-gray-600 dark:text-slate-400">SW Pattern:</span>
|
||||||
|
<input class="error-sw-mask font-mono text-xs w-16 border border-gray-300 dark:border-slate-600 rounded px-1 py-1 dark:bg-slate-700 dark:text-slate-300" placeholder="XXXX" maxlength="4">
|
||||||
|
<span class="text-xs text-gray-500">=</span>
|
||||||
|
<input class="error-sw-value font-mono text-xs w-16 border border-gray-300 dark:border-slate-600 rounded px-1 py-1 dark:bg-slate-700 dark:text-slate-300" placeholder="6A82" maxlength="4">
|
||||||
|
<span class="text-xs text-gray-400">(hex)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2 items-center mt-2">
|
||||||
|
<span class="text-xs text-gray-600 dark:text-slate-400">Command #:</span>
|
||||||
|
<input class="error-cmd-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" type="number" min="1" max="255" placeholder="1">
|
||||||
|
<span class="text-xs text-gray-400">(optional)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 text-xs text-gray-500 dark:text-slate-400">
|
||||||
|
Mask: <code class="error-sw-mask-display font-mono">FFFF</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border border-gray-300 dark:border-slate-600 rounded p-2">
|
||||||
|
<div class="text-xs mb-2 font-medium text-gray-700 dark:text-slate-300">Recovery Action:</div>
|
||||||
|
<div class="error-recovery-builder"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<span class="text-xs text-gray-600 dark:text-slate-400">Retry Count:</span>
|
||||||
|
<input class="error-retry-count 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" type="number" min="0" max="255" value="0">
|
||||||
|
<span class="text-xs text-gray-400">(0=no retry)</span>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
function onBerSubTypeChange(sel) {
|
function onBerSubTypeChange(sel) {
|
||||||
const body = sel.closest('.ber-sub-body') || sel.parentElement.querySelector('.ber-sub-body');
|
const body = sel.closest('.ber-sub-body') || sel.parentElement.querySelector('.ber-sub-body');
|
||||||
const st = sel.value;
|
const st = sel.value;
|
||||||
|
|
||||||
if (st === 'action') {
|
if (st === 'action') {
|
||||||
|
const row = sel.closest('.ber-row');
|
||||||
|
const type = row.querySelector('.ber-type').value;
|
||||||
|
|
||||||
|
if (type === 'error') {
|
||||||
|
body.innerHTML = buildErrorActionRow(row.dataset.berIdx);
|
||||||
|
const recoveryBuilder = body.querySelector('.error-recovery-builder');
|
||||||
|
const recoveryHtml = berRowHtml(0, 'immediate');
|
||||||
|
const tempDiv = document.createElement('div');
|
||||||
|
tempDiv.innerHTML = recoveryHtml;
|
||||||
|
const recoveryBody = tempDiv.querySelector('.ber-body');
|
||||||
|
const recoverySubBody = recoveryBody.querySelector('.ber-sub-body');
|
||||||
|
const recoverySubtype = recoverySubBody.querySelector('.ber-subtype');
|
||||||
|
recoverySubtype.value = 'proactive';
|
||||||
|
onBerSubTypeChange(recoverySubtype);
|
||||||
|
} else {
|
||||||
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)">
|
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="81">81 (Session indication)</option>
|
||||||
<option value="82">82 (Early response)</option>
|
<option value="82">82 (Early response)</option>
|
||||||
</select>`;
|
</select>`;
|
||||||
|
}
|
||||||
} else if (st === 'custom') {
|
} 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)">`;
|
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 {
|
} else {
|
||||||
@@ -2157,7 +2233,13 @@ function updateBerRow(el) {
|
|||||||
function genBerRowValue(row) {
|
function genBerRowValue(row) {
|
||||||
const type = row.querySelector('.ber-type').value;
|
const type = row.querySelector('.ber-type').value;
|
||||||
const tag = BER_TAGS[type];
|
const tag = BER_TAGS[type];
|
||||||
if (type === 'c-apdu' || type === 'chaining') {
|
if (type === 'error') {
|
||||||
|
return genErrorActionValue(row, tag);
|
||||||
|
}
|
||||||
|
if (type === 'chaining') {
|
||||||
|
return genScriptChainingValue(row, tag);
|
||||||
|
}
|
||||||
|
if (type === 'c-apdu') {
|
||||||
const hex = (row.querySelector('.ber-hex').value||'').replace(/[^0-9a-fA-F]/g,'');
|
const hex = (row.querySelector('.ber-hex').value||'').replace(/[^0-9a-fA-F]/g,'');
|
||||||
if (!hex) return '';
|
if (!hex) return '';
|
||||||
return tag + berLenStr(hex.length/2) + hex;
|
return tag + berLenStr(hex.length/2) + hex;
|
||||||
@@ -2175,6 +2257,158 @@ function genBerRowValue(row) {
|
|||||||
return genBerPcValue(row, tag);
|
return genBerPcValue(row, tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function genErrorActionValue(row, tag) {
|
||||||
|
const actionType = row.querySelector('.error-action-type');
|
||||||
|
const triggerType = row.querySelector('.error-trigger-type');
|
||||||
|
const maskInput = row.querySelector('.error-sw-mask');
|
||||||
|
const valueInput = row.querySelector('.error-sw-value');
|
||||||
|
const cmdNumInput = row.querySelector('.error-cmd-num');
|
||||||
|
const recoveryBuilder = row.querySelector('.error-recovery-builder');
|
||||||
|
const retryCount = row.querySelector('.error-retry-count');
|
||||||
|
|
||||||
|
if (!actionType) return '';
|
||||||
|
|
||||||
|
let value = actionType.value;
|
||||||
|
|
||||||
|
const cmdNum = cmdNumInput.value ? parseInt(cmdNumInput.value) : 0xFF;
|
||||||
|
if (triggerType.value === 'cmd-number') {
|
||||||
|
value += cmdNum.toString(16).padStart(2, '0').toUpperCase();
|
||||||
|
} else {
|
||||||
|
value += 'FF';
|
||||||
|
}
|
||||||
|
|
||||||
|
let swMask = '0000';
|
||||||
|
let swValue = '0000';
|
||||||
|
|
||||||
|
switch (triggerType.value) {
|
||||||
|
case 'any-sw':
|
||||||
|
swMask = '0000';
|
||||||
|
swValue = '0000';
|
||||||
|
break;
|
||||||
|
case 'specific-sw':
|
||||||
|
swMask = 'FFFF';
|
||||||
|
swValue = (valueInput.value || '').replace(/[^0-9a-fA-F]/g, '').padStart(4, '0');
|
||||||
|
break;
|
||||||
|
case 'sw-range':
|
||||||
|
swMask = (maskInput.value || '').replace(/[^0-9a-fA-F]/g, '').padStart(4, '0') || '6A00';
|
||||||
|
swValue = (valueInput.value || '').replace(/[^0-9a-fA-F]/g, '').padStart(4, '0') || '6A82';
|
||||||
|
break;
|
||||||
|
case 'cmd-number':
|
||||||
|
swMask = '0000';
|
||||||
|
swValue = '0000';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
value += swMask + swValue;
|
||||||
|
|
||||||
|
if (recoveryBuilder) {
|
||||||
|
const recoveryValue = genBerPcValue(recoveryBuilder.querySelector('.ber-pc'), '81');
|
||||||
|
if (recoveryValue) {
|
||||||
|
value += recoveryValue.slice(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const retry = parseInt(retryCount.value);
|
||||||
|
if (!isNaN(retry) && retry > 0) {
|
||||||
|
value += retry.toString(16).padStart(2, '0').toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
return tag + berLenStr(value.length/2) + value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildScriptChainingRow(rowIdx, hint = '') {
|
||||||
|
return `
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex gap-4 items-center">
|
||||||
|
<span class="text-xs font-medium text-gray-700 dark:text-slate-300">Position:</span>
|
||||||
|
<label class="flex items-center gap-1 text-xs text-gray-600 dark:text-slate-400">
|
||||||
|
<input type="radio" name="chaining-${rowIdx}" value="first" ${hint === 'first' ? 'checked' : ''} onchange="updateChainingFlags(${rowIdx})">
|
||||||
|
First Script
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-1 text-xs text-gray-600 dark:text-slate-400">
|
||||||
|
<input type="radio" name="chaining-${rowIdx}" value="intermediary" ${hint === 'intermediary' ? 'checked' : ''} onchange="updateChainingFlags(${rowIdx})">
|
||||||
|
Intermediary Script
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-1 text-xs text-gray-600 dark:text-slate-400">
|
||||||
|
<input type="checkbox" name="chaining-${rowIdx}-last" value="last" ${hint === 'last' ? 'checked' : ''} onchange="updateChainingFlags(${rowIdx})">
|
||||||
|
Also Last Script
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<span class="text-xs text-gray-600 dark:text-slate-400">Script ID:</span>
|
||||||
|
<input class="chaining-script-id font-mono text-xs w-20 border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-700 dark:text-slate-300"
|
||||||
|
placeholder="${hint || ''}"
|
||||||
|
maxlength="8"
|
||||||
|
oninput="updateBerRow(this)">
|
||||||
|
<span class="text-xs text-gray-400">(hex, 1-4 bytes)</span>
|
||||||
|
<button type="button" class="text-xs text-blue-600 hover:underline"
|
||||||
|
onclick="generateScriptIdHint(${rowIdx})">Hint</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<span class="text-xs text-gray-600 dark:text-slate-400">Additional Data:</span>
|
||||||
|
<input class="chaining-additional font-mono text-xs flex-1 border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-700 dark:text-slate-300"
|
||||||
|
placeholder="hex (optional)"
|
||||||
|
oninput="updateBerRow(this)">
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateChainingFlags(rowIdx) {
|
||||||
|
const first = document.querySelector(`input[name="chaining-${rowIdx}"][value="first"]`);
|
||||||
|
const intermediate = document.querySelector(`input[name="chaining-${rowIdx}"][value="intermediary"]`);
|
||||||
|
if (first && intermediate) {
|
||||||
|
if (first.checked) intermediate.checked = false;
|
||||||
|
if (intermediate.checked) first.checked = false;
|
||||||
|
}
|
||||||
|
updateBerRow(document.querySelector(`[data-ber-idx="${rowIdx}"] .ber-type`));
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateScriptIdHint(rowIdx) {
|
||||||
|
const row = document.querySelector(`[data-ber-idx="${rowIdx}"]`);
|
||||||
|
if (!row) return;
|
||||||
|
const scriptIdInput = row.querySelector('.chaining-script-id');
|
||||||
|
if (!scriptIdInput) return;
|
||||||
|
|
||||||
|
const allIds = Array.from(document.querySelectorAll('.chaining-script-id'))
|
||||||
|
.map(input => input.value || input.placeholder)
|
||||||
|
.filter(id => id && id !== '');
|
||||||
|
|
||||||
|
const lastId = allIds.length ? allIds[allIds.length - 1] : '00000000';
|
||||||
|
const num = parseInt(lastId, 16) + 1;
|
||||||
|
const nextId = num.toString(16).padStart(8, '0').toUpperCase();
|
||||||
|
scriptIdInput.placeholder = nextId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function genScriptChainingValue(row, tag) {
|
||||||
|
const firstRadio = row.querySelector(`input[name="chaining-"][value="first"]`);
|
||||||
|
const intermediateRadio = row.querySelector(`input[name="chaining-"][value="intermediary"]`);
|
||||||
|
const lastCheckbox = row.querySelector(`input[name="chaining-]-last"]`);
|
||||||
|
const scriptIdInput = row.querySelector('.chaining-script-id');
|
||||||
|
const additionalInput = row.querySelector('.chaining-additional');
|
||||||
|
|
||||||
|
let flags = 0;
|
||||||
|
if (firstRadio && firstRadio.checked) flags |= 0x01;
|
||||||
|
if (intermediateRadio && intermediateRadio.checked) flags |= 0x02;
|
||||||
|
if (lastCheckbox && lastCheckbox.checked) flags |= 0x04;
|
||||||
|
|
||||||
|
let value = flags.toString(16).padStart(2, '0').toUpperCase();
|
||||||
|
|
||||||
|
const scriptId = (scriptIdInput.value || '').replace(/[^0-9a-fA-F]/g, '');
|
||||||
|
if (scriptId) {
|
||||||
|
value += scriptId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const additional = (additionalInput.value || '').replace(/[^0-9a-fA-F]/g, '');
|
||||||
|
if (additional) {
|
||||||
|
value += additional;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value || value === '00') return '';
|
||||||
|
return tag + berLenStr(value.length/2) + value;
|
||||||
|
}
|
||||||
|
|
||||||
function genBerPcValue(row, tag) {
|
function genBerPcValue(row, tag) {
|
||||||
const pc = row.querySelector('.ber-pc');
|
const pc = row.querySelector('.ber-pc');
|
||||||
const pct = pc.querySelector('.ber-pc-type').value;
|
const pct = pc.querySelector('.ber-pc-type').value;
|
||||||
@@ -2229,6 +2463,44 @@ function berLenStr(n) {
|
|||||||
return '81' + n.toString(16).padStart(2,'0').toUpperCase();
|
return '81' + n.toString(16).padStart(2,'0').toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateSwMaskDisplay(row) {
|
||||||
|
const triggerType = row.querySelector('.error-trigger-type');
|
||||||
|
if (!triggerType) return;
|
||||||
|
const maskInput = row.querySelector('.error-sw-mask');
|
||||||
|
const valueInput = row.querySelector('.error-sw-value');
|
||||||
|
const display = row.querySelector('.error-sw-mask-display');
|
||||||
|
|
||||||
|
let mask = '0000';
|
||||||
|
|
||||||
|
switch (triggerType.value) {
|
||||||
|
case 'any-sw':
|
||||||
|
mask = '0000';
|
||||||
|
break;
|
||||||
|
case 'specific-sw':
|
||||||
|
mask = 'FFFF';
|
||||||
|
break;
|
||||||
|
case 'sw-range':
|
||||||
|
mask = maskInput.value || '6A00';
|
||||||
|
break;
|
||||||
|
case 'cmd-number':
|
||||||
|
mask = '0000';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
display.textContent = mask.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.addEventListener('change', (e) => {
|
||||||
|
if (e.target.classList.contains('error-trigger-type') ||
|
||||||
|
e.target.classList.contains('error-sw-mask')) {
|
||||||
|
const row = e.target.closest('.ber-row');
|
||||||
|
if (row) updateSwMaskDisplay(row);
|
||||||
|
updateBerRow(e.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function genBerTlv() {
|
function genBerTlv() {
|
||||||
const fmt = document.getElementById('ber-format').value;
|
const fmt = document.getElementById('ber-format').value;
|
||||||
let content = '';
|
let content = '';
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ def _ota_reference(spi1, spi2, kic, kid, tar_hex, cntr_hex, apdu_hex, kic_key_he
|
|||||||
|
|
||||||
|
|
||||||
def _decode_por(spi1, spi2, kic, kid, cntr_hex, kic_key_hex, kid_key_hex, response_hex):
|
def _decode_por(spi1, spi2, kic, kid, cntr_hex, kic_key_hex, kid_key_hex, response_hex):
|
||||||
from pySim.ota import OtaDialectSms
|
from pySim.ota import OtaDialectSms, CompactRemoteResp
|
||||||
from osmocom.utils import h2b, b2h
|
from osmocom.utils import h2b, b2h
|
||||||
if not response_hex:
|
if not response_hex:
|
||||||
return None
|
return None
|
||||||
@@ -336,12 +336,67 @@ def _decode_por(spi1, spi2, kic, kid, cntr_hex, kic_key_hex, kid_key_hex, respon
|
|||||||
'tar': res['tar'].hex().upper(),
|
'tar': res['tar'].hex().upper(),
|
||||||
'pcntr': res['pcntr'],
|
'pcntr': res['pcntr'],
|
||||||
}
|
}
|
||||||
if dec is not None:
|
|
||||||
out['decoded'] = {
|
# Try ExpandedRemoteResponse first (TS 102 226 §5.2.2)
|
||||||
'number_of_commands': dec['number_of_commands'],
|
if res.response_status == 'por_ok' and len(res['secured_data']):
|
||||||
'last_status_word': str(dec['last_status_word']),
|
try:
|
||||||
'last_response_data': str(dec['last_response_data']),
|
from construct import Struct, Int8ub, Bytes, GreedyBytes, Optional, Array, this
|
||||||
|
ExpandedRemoteResponse = Struct(
|
||||||
|
'response_count'/Int8ub,
|
||||||
|
'responses'/Array(this.response_count, Struct(
|
||||||
|
'command_number'/Int8ub,
|
||||||
|
'status_word'/Bytes(2),
|
||||||
|
'response_data'/GreedyBytes,
|
||||||
|
'error_details'/Optional(Struct(
|
||||||
|
'error_code'/Int8ub,
|
||||||
|
'error_info'/GreedyBytes
|
||||||
|
)),
|
||||||
|
'chaining_context'/Optional(Struct(
|
||||||
|
'script_id'/Bytes(4),
|
||||||
|
'is_first'/Int8ub,
|
||||||
|
'is_last'/Int8ub,
|
||||||
|
))
|
||||||
|
))
|
||||||
|
)
|
||||||
|
expanded = ExpandedRemoteResponse.parse(res['secured_data'])
|
||||||
|
out['response_type'] = 'expanded'
|
||||||
|
out['response_count'] = expanded.response_count
|
||||||
|
out['responses'] = []
|
||||||
|
for resp in expanded.responses:
|
||||||
|
response_data = {
|
||||||
|
'command_number': resp.command_number,
|
||||||
|
'status_word': resp.status_word.hex().upper(),
|
||||||
|
'response_data': b2h(resp.response_data).upper() if resp.response_data else '',
|
||||||
}
|
}
|
||||||
|
if resp.error_details:
|
||||||
|
response_data['error_code'] = resp.error_details.error_code
|
||||||
|
response_data['error_info'] = b2h(resp.error_details.error_info).upper()
|
||||||
|
if resp.chaining_context:
|
||||||
|
response_data['script_id'] = resp.chaining_context.script_id.hex().upper()
|
||||||
|
response_data['is_first'] = resp.chaining_context.is_first == 0x01
|
||||||
|
response_data['is_last'] = resp.chaining_context.is_last == 0x01
|
||||||
|
out['responses'].append(response_data)
|
||||||
|
except Exception:
|
||||||
|
# Fallback to CompactRemoteResp
|
||||||
|
if dec is not None:
|
||||||
|
out['response_type'] = 'compact'
|
||||||
|
out['decoded'] = {
|
||||||
|
'number_of_commands': dec.number_of_commands,
|
||||||
|
'last_status_word': str(dec.last_status_word),
|
||||||
|
'last_response_data': str(dec.last_response_data),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
out['response_type'] = 'none'
|
||||||
|
elif dec is not None:
|
||||||
|
out['response_type'] = 'compact'
|
||||||
|
out['decoded'] = {
|
||||||
|
'number_of_commands': dec.number_of_commands,
|
||||||
|
'last_status_word': str(dec.last_status_word),
|
||||||
|
'last_response_data': str(dec.last_response_data),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
out['response_type'] = 'none'
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -336,5 +336,63 @@ class TestProactiveDecode(unittest.TestCase):
|
|||||||
self.assertNotIn('tr_result', entry)
|
self.assertNotIn('tr_result', entry)
|
||||||
|
|
||||||
|
|
||||||
|
class TestExpandedRemoteResponse(unittest.TestCase):
|
||||||
|
"""Expanded Remote Response parsing (TS 102 226 §5.2.2)."""
|
||||||
|
|
||||||
|
def test_expanded_response_single_command(self):
|
||||||
|
entry = {'type_hex': '03', 'qualifier': None}
|
||||||
|
_record_tr(entry, bytes.fromhex('810301030082028183030100'))
|
||||||
|
self.assertEqual(entry['tr_result'], '00')
|
||||||
|
self.assertEqual(entry['tr_result_name'], 'Command performed successfully')
|
||||||
|
|
||||||
|
def test_expanded_response_parser_single_command(self):
|
||||||
|
# Simple test of the construct parsing structure
|
||||||
|
try:
|
||||||
|
from construct import Struct, Int8ub, Bytes, GreedyBytes, Optional, Array, this
|
||||||
|
from osmocom.utils import b2h
|
||||||
|
|
||||||
|
# Create sample expanded response data
|
||||||
|
secured_data = bytes.fromhex('01' '01' '9000' '11') # response_count, cmd#, SW, data
|
||||||
|
|
||||||
|
ExpandedRemoteResponse = Struct(
|
||||||
|
'response_count'/Int8ub,
|
||||||
|
'responses'/Array(this.response_count, Struct(
|
||||||
|
'command_number'/Int8ub,
|
||||||
|
'status_word'/Bytes(2),
|
||||||
|
'response_data'/GreedyBytes,
|
||||||
|
'error_details'/Optional(Struct(
|
||||||
|
'error_code'/Int8ub,
|
||||||
|
'error_info'/GreedyBytes
|
||||||
|
)),
|
||||||
|
'chaining_context'/Optional(Struct(
|
||||||
|
'script_id'/Bytes(4),
|
||||||
|
'is_first'/Int8ub,
|
||||||
|
'is_last'/Int8ub,
|
||||||
|
))
|
||||||
|
))
|
||||||
|
)
|
||||||
|
expanded = ExpandedRemoteResponse.parse(secured_data)
|
||||||
|
self.assertEqual(expanded.response_count, 1)
|
||||||
|
self.assertEqual(expanded.responses[0].command_number, 1)
|
||||||
|
self.assertEqual(b2h(expanded.responses[0].status_word).upper(), '9000')
|
||||||
|
self.assertEqual(b2h(expanded.responses[0].response_data).upper(), '11')
|
||||||
|
except Exception as e:
|
||||||
|
self.fail(f"ExpandedRemoteResponse parsing failed: {e}")
|
||||||
|
|
||||||
|
def test_expanded_response_with_error(self):
|
||||||
|
entry = {'type_hex': '26', 'qualifier': '01'}
|
||||||
|
# Result TLV: 03 01 6A (error_code 0x6A)
|
||||||
|
_record_tr(entry, bytes.fromhex('81030326008202818303016A'))
|
||||||
|
self.assertEqual(entry['tr_result'], '6a')
|
||||||
|
self.assertEqual(entry['tr_result_name'], 'Command performed with limited understanding')
|
||||||
|
|
||||||
|
def test_expanded_response_with_chaining(self):
|
||||||
|
entry = {'type_hex': '26', 'qualifier': '00'}
|
||||||
|
# Result: 03 01 00 + chaining context with script_id
|
||||||
|
_record_tr(entry, bytes.fromhex('81030326008202818303010093'))
|
||||||
|
self.assertEqual(entry['tr_result'], '00')
|
||||||
|
self.assertEqual(entry['tr_result_name'], 'Command performed successfully')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user