http ota: spec-correct retry timer, TLV and APDU edges (v2.2.12)
Audit against GP v2.2 Am.B 4.7 / TS 102 226 / GP Card Spec 11.11 found the retry waiting delay encoded as plain hex instead of the TP-SCTS semi-octet order required by TS 102 223 8.38 -> TS 23.040 9.1.2.3 (1 min must be 10, 20 s must be 02); the builder now encodes semi-octets, clamps 0-59 / 0-99 and pads the 2-byte counter. Odd-length hex is padded instead of producing fractional BER lengths; empty 83/84/89 sub-TLVs are omitted (lengths are 1-n per Tables 4-3/4-5/4-8..10) and an empty trigger becomes 81 00; a store payload above a short APDU is chained as P1.b8=0 STORE DATA blocks (P2 = block number). Connection presets fixed: device identities 82, alpha 05, command details 81, bearer 35/03; the A5 store tag and the 'B0,00=unlimited' counter hint are marked unverified (not in the pinned spec). Docs: UICC_SPECS.md 9.6.5 example annotated 2 s, findings 10-minute timer corrected to 1 minute; help updated. SW cache otaman-v177.
This commit is contained in:
@@ -51,7 +51,9 @@ continuation GET STATUS (P2=02 with the last AID as search criterion).
|
|||||||
- `80CA008500` (GET DATA HTTP administration parameters) **works** and returns
|
- `80CA008500` (GET DATA HTTP administration parameters) **works** and returns
|
||||||
the SD's stored OTA configuration: `8A 09 "localhost"`, `8B 14 <agent id>`,
|
the SD's stored OTA configuration: `8A 09 "localhost"`, `8B 14 <agent id>`,
|
||||||
`8C 01 "/"` (stored URI), `85 14 <PSK identity>`, `86 07 00 01 25 03 00 10 00`
|
`8C 01 "/"` (stored URI), `85 14 <PSK identity>`, `86 07 00 01 25 03 00 10 00`
|
||||||
(retry counter 1, timer **10 minutes**), `02 40 01` (KVN/KID), APN-ish
|
(retry counter 1, timer **1 minute** — the `25 03` h:m:s fields are TP-SCTS
|
||||||
|
semi-octets, TS 23.040 §9.1.2.3: low nibble = most significant digit, so
|
||||||
|
`10` = `01` min), `02 40 01` (KVN/KID), APN-ish
|
||||||
`C7 04 03 47 50 42`, destination `BE 05 21 5B D5 05 02` = 91.213.5.2.
|
`C7 04 03 47 50 42`, destination `BE 05 21 5B D5 05 02` = 91.213.5.2.
|
||||||
- `80F28002/80F24002/80F21002 ...4F0000` return `6985` through the reader when
|
- `80F28002/80F24002/80F21002 ...4F0000` return `6985` through the reader when
|
||||||
the ISD is not the current DF; the reference platform sends
|
the ISD is not the current DF; the reference platform sends
|
||||||
@@ -64,7 +66,8 @@ continuation GET STATUS (P2=02 with the last AID as search criterion).
|
|||||||
`0A` channel status.
|
`0A` channel status.
|
||||||
- A Location status event re-triggers the OTA session only while the last
|
- A Location status event re-triggers the OTA session only while the last
|
||||||
session is incomplete; after a clean session end the card waits for a push.
|
session is incomplete; after a clean session end the card waits for a push.
|
||||||
- The SD stores a 10-minute retry timer (`25 03 00 10 00`).
|
- The SD stores a 1-minute retry timer (`25 03 00 10 00`; timer fields are
|
||||||
|
TP-SCTS semi-octets per TS 102 223 §8.38 / TS 23.040 §9.1.2.3).
|
||||||
|
|
||||||
## Confirmed rules (with evidence)
|
## Confirmed rules (with evidence)
|
||||||
|
|
||||||
|
|||||||
@@ -212,15 +212,15 @@
|
|||||||
<p class="text-sm mb-3">Сборка payload’ов Remote Application Management over HTTP по GlobalPlatform <strong>GPC v2.2 Amendment B v1.1</strong> (§4.7). Два режима:</p>
|
<p class="text-sm mb-3">Сборка payload’ов Remote Application Management over HTTP по GlobalPlatform <strong>GPC v2.2 Amendment B v1.1</strong> (§4.7). Два режима:</p>
|
||||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||||
<li><strong>Триггер (Push SMS)</strong> — параметры запуска административной сессии (<code class="font-mono text-sm">81 > 83 > 84/[85]/[86]/89</code>, таблица 4-3). Сообщение, которое просит Security Domain карты исходящим запросом начать HTTP-сессию.</li>
|
<li><strong>Триггер (Push SMS)</strong> — параметры запуска административной сессии (<code class="font-mono text-sm">81 > 83 > 84/[85]/[86]/89</code>, таблица 4-3). Сообщение, которое просит Security Domain карты исходящим запросом начать HTTP-сессию.</li>
|
||||||
<li><strong>Store (параметры SD)</strong> — записывает те же параметры как данные карты (параметры Security Domain) командой <strong>STORE DATA в TLV-режиме</strong> (<code class="font-mono text-sm">80 E2 90 00</code>, P1=90 = последний блок + BER-TLV по GP v2.2 Amendment B v1.1.3), обёрнутые в тег <code class="font-mono text-sm">85</code> (или <code class="font-mono text-sm">A5</code>) по таблице 4-4.</li>
|
<li><strong>Store (параметры SD)</strong> — записывает те же параметры как данные карты (параметры Security Domain) командой <strong>STORE DATA в TLV-режиме</strong> (<code class="font-mono text-sm">80 E2 90 00</code>, P1=90 = последний блок + без шифрования + BER-TLV по GP Card Spec v2.3.1 §11.11; данные длиннее короткого APDU разбиваются на цепочку блоков с P1.b8=0), обёрнутые в тег <code class="font-mono text-sm">85</code> по таблице 4-4 (<code class="font-mono text-sm">A5</code> — непроверенный вариант).</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="text-sm mb-2">Разделы соответствуют таблицам спецификации:</p>
|
<p class="text-sm mb-2">Разделы соответствуют таблицам спецификации:</p>
|
||||||
<table class="w-full text-sm mb-3 border-collapse">
|
<table class="w-full text-sm mb-3 border-collapse">
|
||||||
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Раздел</th><th class="text-left py-1 px-2">Tag</th><th class="text-left py-1 px-2">Содержимое</th></tr></thead>
|
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Раздел</th><th class="text-left py-1 px-2">Tag</th><th class="text-left py-1 px-2">Содержимое</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Параметры соединения</td><td class="py-1 px-2 font-mono">84</td><td class="py-1 px-2">Любые COMPREHENSION-TLV для открытия TCP-соединения (OPEN CHANNEL по TS 102 223): Device Identities <code class="font-mono text-sm">02</code>, Alpha <code class="font-mono text-sm">80</code>, Bearer <code class="font-mono text-sm">01</code>, вендорские TLV. Редактор строк + пресеты, редактируемый hex.</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Параметры соединения</td><td class="py-1 px-2 font-mono">84</td><td class="py-1 px-2">Любые COMPREHENSION-TLV для открытия TCP-соединения (OPEN CHANNEL по TS 102 223): Device Identities <code class="font-mono text-sm">82</code>, Alpha <code class="font-mono text-sm">05</code>, Command details <code class="font-mono text-sm">81</code>, Bearer <code class="font-mono text-sm">35</code> (<code class="font-mono text-sm">03</code> = bearer по умолчанию), вендорские TLV. Редактор строк + пресеты, редактируемый hex.</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Параметры безопасности</td><td class="py-1 px-2 font-mono">85</td><td class="py-1 px-2">Таблица 4-6: LV PSK Identity (текст), LV Key version/KID. Идентифицирует ключ PSK TLS (RFC 4279).</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Параметры безопасности</td><td class="py-1 px-2 font-mono">85</td><td class="py-1 px-2">Таблица 4-6: LV PSK Identity (текст), LV Key version/KID. Идентифицирует ключ PSK TLS (RFC 4279).</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Политика повтора</td><td class="py-1 px-2 font-mono">86</td><td class="py-1 px-2">Таблица 4-7: счётчик повторов (2 байта, напр. <code class="font-mono text-sm">B000</code>), задержка повторной попытки как timer TLV из TS 102 223 (<code class="font-mono text-sm">25 03 HH MM SS</code>), опциональный вендорский TLV отчёта о сбое.</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Политика повтора</td><td class="py-1 px-2 font-mono">86</td><td class="py-1 px-2">Таблица 4-7: счётчик повторов (2 байта, напр. <code class="font-mono text-sm">0003</code>), задержка повторной попытки как timer TLV из TS 102 223 (<code class="font-mono text-sm">25 03 HH MM SS</code>) с полями в полуоктетном порядке TP-SCTS (TS 23.040 §9.1.2.3): <code class="font-mono text-sm">00 10 00</code> = 1 мин, <code class="font-mono text-sm">00 00 02</code> = 20 с; опциональный TLV отчёта о сбое.</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">HTTP POST</td><td class="py-1 px-2 font-mono">89</td><td class="py-1 px-2">Таблицы 4-8/9/10: Host-заголовок (<code class="font-mono text-sm">8A</code>), X-Admin-From agent ID (<code class="font-mono text-sm">8B</code>), URI (<code class="font-mono text-sm">8C</code>) — текст преобразуется в октеты.</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">HTTP POST</td><td class="py-1 px-2 font-mono">89</td><td class="py-1 px-2">Таблицы 4-8/9/10: Host-заголовок (<code class="font-mono text-sm">8A</code>), X-Admin-From agent ID (<code class="font-mono text-sm">8B</code>), URI (<code class="font-mono text-sm">8C</code>) — текст преобразуется в октеты.</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
+3
-3
@@ -212,15 +212,15 @@
|
|||||||
<p class="text-sm mb-3">Builds the Remote Application Management over HTTP payloads defined in GlobalPlatform <strong>GPC v2.2 Amendment B v1.1</strong> (§4.7). Two modes:</p>
|
<p class="text-sm mb-3">Builds the Remote Application Management over HTTP payloads defined in GlobalPlatform <strong>GPC v2.2 Amendment B v1.1</strong> (§4.7). Two modes:</p>
|
||||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||||
<li><strong>Trigger (Push SMS)</strong> — administration session triggering parameters (<code class="font-mono text-sm">81 > 83 > 84/[85]/[86]/89</code>, Table 4-3). This is the message that asks the card's Security Domain to dial out and start an HTTP session.</li>
|
<li><strong>Trigger (Push SMS)</strong> — administration session triggering parameters (<code class="font-mono text-sm">81 > 83 > 84/[85]/[86]/89</code>, Table 4-3). This is the message that asks the card's Security Domain to dial out and start an HTTP session.</li>
|
||||||
<li><strong>Store (SD admin params)</strong> — writes the same parameters as card (Security Domain) data via <strong>STORE DATA in TLV mode</strong> (<code class="font-mono text-sm">80 E2 90 00</code>, P1=90 = last block + BER-TLV per GP v2.2 Amendment B v1.1.3), wrapped in tag <code class="font-mono text-sm">85</code> (or <code class="font-mono text-sm">A5</code>) per Table 4-4.</li>
|
<li><strong>Store (SD admin params)</strong> — writes the same parameters as card (Security Domain) data via <strong>STORE DATA in TLV mode</strong> (<code class="font-mono text-sm">80 E2 90 00</code>, P1=90 = last block + no encryption + BER-TLV per GP Card Spec v2.3.1 §11.11; data above a short APDU is chained as P1.b8=0 blocks), wrapped in tag <code class="font-mono text-sm">85</code> per Table 4-4 (<code class="font-mono text-sm">A5</code> is offered as an unverified variant).</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="text-sm mb-2">Sections mirror the spec tables:</p>
|
<p class="text-sm mb-2">Sections mirror the spec tables:</p>
|
||||||
<table class="w-full text-sm mb-3 border-collapse">
|
<table class="w-full text-sm mb-3 border-collapse">
|
||||||
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Section</th><th class="text-left py-1 px-2">Tag</th><th class="text-left py-1 px-2">Contents</th></tr></thead>
|
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Section</th><th class="text-left py-1 px-2">Tag</th><th class="text-left py-1 px-2">Contents</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Connection parameters</td><td class="py-1 px-2 font-mono">84</td><td class="py-1 px-2">Any COMPREHENSION-TLV needed to open the TCP connection (OPEN CHANNEL per TS 102 223): Device Identities <code class="font-mono text-sm">02</code>, Alpha <code class="font-mono text-sm">80</code>, Bearer <code class="font-mono text-sm">01</code>, vendor TLVs. Row editor + presets, editable hex.</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Connection parameters</td><td class="py-1 px-2 font-mono">84</td><td class="py-1 px-2">Any COMPREHENSION-TLV needed to open the TCP connection (OPEN CHANNEL per TS 102 223): Device Identities <code class="font-mono text-sm">82</code>, Alpha <code class="font-mono text-sm">05</code>, Command details <code class="font-mono text-sm">81</code>, Bearer <code class="font-mono text-sm">35</code> (<code class="font-mono text-sm">03</code> = default bearer), vendor TLVs. Row editor + presets, editable hex.</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Security parameters</td><td class="py-1 px-2 font-mono">85</td><td class="py-1 px-2">Table 4-6: LV PSK Identity (text), LV Key version/KID. Identifies the PSK TLS key (RFC 4279).</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Security parameters</td><td class="py-1 px-2 font-mono">85</td><td class="py-1 px-2">Table 4-6: LV PSK Identity (text), LV Key version/KID. Identifies the PSK TLS key (RFC 4279).</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Retry policy</td><td class="py-1 px-2 font-mono">86</td><td class="py-1 px-2">Table 4-7: retry counter (2 bytes, e.g. <code class="font-mono text-sm">B000</code>), retry waiting delay as the TS 102 223 timer TLV (<code class="font-mono text-sm">25 03 HH MM SS</code>), optional vendor-specific report-failure TLV.</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Retry policy</td><td class="py-1 px-2 font-mono">86</td><td class="py-1 px-2">Table 4-7: retry counter (2 bytes, e.g. <code class="font-mono text-sm">0003</code>), retry waiting delay as the TS 102 223 timer TLV (<code class="font-mono text-sm">25 03 HH MM SS</code>) with TP-SCTS semi-octet fields (TS 23.040 §9.1.2.3): <code class="font-mono text-sm">00 10 00</code> = 1 min, <code class="font-mono text-sm">00 00 02</code> = 20 s; optional report-failure TLV.</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">HTTP POST</td><td class="py-1 px-2 font-mono">89</td><td class="py-1 px-2">Tables 4-8/9/10: Host header (<code class="font-mono text-sm">8A</code>), X-Admin-From agent ID (<code class="font-mono text-sm">8B</code>), URI (<code class="font-mono text-sm">8C</code>) — text converted to octets.</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">HTTP POST</td><td class="py-1 px-2 font-mono">89</td><td class="py-1 px-2">Tables 4-8/9/10: Host header (<code class="font-mono text-sm">8A</code>), X-Admin-From agent ID (<code class="font-mono text-sm">8B</code>), URI (<code class="font-mono text-sm">8C</code>) — text converted to octets.</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
+56
-32
@@ -18,7 +18,7 @@
|
|||||||
<div class="max-w-7xl mx-auto px-6 py-2">
|
<div class="max-w-7xl mx-auto px-6 py-2">
|
||||||
|
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="flex items-center justify-between mb-3">
|
||||||
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v2.2.11</span></h1>
|
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v2.2.12</span></h1>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<span id="state-indicator" class="flex items-center select-none" style="cursor:default" title="Connecting...">
|
<span id="state-indicator" class="flex items-center select-none" style="cursor:default" title="Connecting...">
|
||||||
<span id="state-indicator-dot" class="text-xs text-gray-400" title="Connecting...">●</span>
|
<span id="state-indicator-dot" class="text-xs text-gray-400" title="Connecting...">●</span>
|
||||||
@@ -174,7 +174,7 @@
|
|||||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="SD params tag">SD params tag</label>
|
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="SD params tag">SD params tag</label>
|
||||||
<select id="hota-store-tag" onchange="hotaUpdatePreview()" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800">
|
<select id="hota-store-tag" onchange="hotaUpdatePreview()" 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="85">'85' — GP v2.2 Amendment B v1.1 (Table 4-4)</option>
|
<option value="85">'85' — GP v2.2 Amendment B v1.1 (Table 4-4)</option>
|
||||||
<option value="A5">'A5' — GP v2.2 Amendment B v1.1.3</option>
|
<option value="A5">'A5' — not in the pinned v1.1 spec (unverified)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -188,7 +188,8 @@
|
|||||||
<button onclick="hotaAddTlv()" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700" data-l10n="+ TLV">+ TLV</button>
|
<button onclick="hotaAddTlv()" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700" data-l10n="+ TLV">+ TLV</button>
|
||||||
<button onclick="hotaAddPreset('devid')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700" data-l10n="+ Device Identities">+ Device Identities</button>
|
<button onclick="hotaAddPreset('devid')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700" data-l10n="+ Device Identities">+ Device Identities</button>
|
||||||
<button onclick="hotaAddPreset('alpha')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700" data-l10n="+ Alpha (empty)">+ Alpha (empty)</button>
|
<button onclick="hotaAddPreset('alpha')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700" data-l10n="+ Alpha (empty)">+ Alpha (empty)</button>
|
||||||
<button onclick="hotaAddPreset('bearer')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700" data-l10n="+ Bearer (GPRS)">+ Bearer (GPRS)</button>
|
<button onclick="hotaAddPreset('cmddet')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700" data-l10n="+ Command details">+ Command details</button>
|
||||||
|
<button onclick="hotaAddPreset('bearer')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700" data-l10n="+ Bearer (default)">+ Bearer (default)</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -219,13 +220,13 @@
|
|||||||
</label>
|
</label>
|
||||||
<div id="hota-retry-fields" class="hidden space-y-2">
|
<div id="hota-retry-fields" class="hidden space-y-2">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<div class="flex-1"><label class="block text-xs text-gray-600 dark:text-slate-400" data-l10n="Retry counter (B0,00=unlimited)">Retry counter (B0,00=unlimited)</label>
|
<div class="flex-1"><label class="block text-xs text-gray-600 dark:text-slate-400" data-l10n="Retry counter (2 bytes, hex)">Retry counter (2 bytes, hex)</label>
|
||||||
<input id="hota-retry-counter" maxlength="4" 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="B000">
|
<input id="hota-retry-counter" maxlength="4" 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="B000">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<div class="w-16"><label class="block text-xs text-gray-600 dark:text-slate-400" data-l10n="Hours">Hours</label>
|
<div class="w-16"><label class="block text-xs text-gray-600 dark:text-slate-400" data-l10n="Hours">Hours</label>
|
||||||
<input id="hota-delay-h" type="number" min="0" max="255" 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="0">
|
<input id="hota-delay-h" type="number" min="0" max="99" 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="0">
|
||||||
</div>
|
</div>
|
||||||
<div class="w-16"><label class="block text-xs text-gray-600 dark:text-slate-400" data-l10n="Minutes">Minutes</label>
|
<div class="w-16"><label class="block text-xs text-gray-600 dark:text-slate-400" data-l10n="Minutes">Minutes</label>
|
||||||
<input id="hota-delay-m" type="number" min="0" max="59" 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="0">
|
<input id="hota-delay-m" type="number" min="0" max="59" 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="0">
|
||||||
@@ -1339,17 +1340,21 @@ function _hotaBerLen(n) {
|
|||||||
if (n < 256) return '81' + n.toString(16).padStart(2, '0').toUpperCase();
|
if (n < 256) return '81' + n.toString(16).padStart(2, '0').toUpperCase();
|
||||||
return '82' + n.toString(16).padStart(4, '0').toUpperCase();
|
return '82' + n.toString(16).padStart(4, '0').toUpperCase();
|
||||||
}
|
}
|
||||||
function _hotaTlv(tag8, val8) { const v = _hotaHex(val8); return tag8.toUpperCase() + _hotaBerLen(v.length / 2) + v; }
|
// Hex normalisation for byte strings: an odd digit count is padded on the
|
||||||
|
// left so a half byte can never corrupt the TLV length field.
|
||||||
|
function _hotaPad(s) { const v = _hotaHex(s); return v.length % 2 ? '0' + v : v; }
|
||||||
|
function _hotaTlv(tag8, val8) { const v = _hotaPad(val8); return _hotaHex(tag8) + _hotaBerLen(v.length / 2) + v; }
|
||||||
|
|
||||||
function hotaTlv(tag, value) { return _hotaTlv(tag, value); }
|
function hotaTlv(tag, value) { return _hotaTlv(tag, value); }
|
||||||
function hotaBuildConn(tlvs) {
|
function hotaBuildConn(tlvs) {
|
||||||
let content = '';
|
let content = '';
|
||||||
for (const t of tlvs) {
|
for (const t of tlvs) {
|
||||||
const v = _hotaHex(t.value);
|
|
||||||
if (!t.tag) continue;
|
if (!t.tag) continue;
|
||||||
content += t.tag.toUpperCase() + _hotaBerLen(v.length / 2) + v;
|
const v = _hotaPad(t.value);
|
||||||
|
content += _hotaHex(t.tag) + _hotaBerLen(v.length / 2) + v;
|
||||||
}
|
}
|
||||||
return '84' + _hotaBerLen(content.length / 2) + content;
|
// '84' is 1-n per Table 4-5: an empty container must be left out entirely
|
||||||
|
return content ? '84' + _hotaBerLen(content.length / 2) + content : '';
|
||||||
}
|
}
|
||||||
function hotaBuildSec(s) {
|
function hotaBuildSec(s) {
|
||||||
const psk = _hotaAsciiHex(s.pskIdentity);
|
const psk = _hotaAsciiHex(s.pskIdentity);
|
||||||
@@ -1359,35 +1364,52 @@ function hotaBuildSec(s) {
|
|||||||
return '85' + _hotaBerLen(data.length / 2) + data;
|
return '85' + _hotaBerLen(data.length / 2) + data;
|
||||||
}
|
}
|
||||||
function hotaBuildRetry(r) {
|
function hotaBuildRetry(r) {
|
||||||
const counter = _hotaHex(r.counter);
|
const counter = _hotaHex(r.counter).padStart(4, '0').slice(-4);
|
||||||
const hh = (parseInt(r.delayH) || 0).toString(16).padStart(2, '0').toUpperCase();
|
// Retry waiting delay = the TS 102 223 '25' timer value; each field uses
|
||||||
const mm = (parseInt(r.delayM) || 0).toString(16).padStart(2, '0').toUpperCase();
|
// the TP-SCTS semi-octet order of TS 23.040 9.1.2.3 (low nibble = most
|
||||||
const ss = (parseInt(r.delayS) || 0).toString(16).padStart(2, '0').toUpperCase();
|
// significant digit), i.e. 1 min = '10', 20 s = '02', 10 min = '01'.
|
||||||
const timerTlv = '2503' + hh + mm + ss;
|
const clamp = (v, max) => Math.min(max, Math.max(0, parseInt(v, 10) || 0));
|
||||||
|
const semi = v => (((v % 10) << 4) | Math.floor(v / 10)).toString(16).padStart(2, '0').toUpperCase();
|
||||||
|
const timerTlv = '2503' + semi(clamp(r.delayH, 99)) + semi(clamp(r.delayM, 59)) + semi(clamp(r.delayS, 59));
|
||||||
const report = _hotaHex(r.reportFailure);
|
const report = _hotaHex(r.reportFailure);
|
||||||
const content = counter + timerTlv + (report && report.length % 2 === 0 ? report : '');
|
const content = counter + timerTlv + (report.length % 2 === 0 ? report : '');
|
||||||
return '86' + _hotaBerLen(content.length / 2) + content;
|
return '86' + _hotaBerLen(content.length / 2) + content;
|
||||||
}
|
}
|
||||||
function hotaBuildHttpPost(h) {
|
function hotaBuildHttpPost(h) {
|
||||||
const host = _hotaAsciiHex(h.host);
|
// '89' is 1-n per Table 4-8/9/10: only non-empty parameters are emitted
|
||||||
const agent = _hotaAsciiHex(h.agent);
|
let content = '';
|
||||||
const uri = _hotaAsciiHex(h.uri);
|
if (_hotaAsciiHex(h.host)) content += _hotaTlv('8A', _hotaAsciiHex(h.host));
|
||||||
const content = _hotaTlv('8A', host) + _hotaTlv('8B', agent) + _hotaTlv('8C', uri);
|
if (_hotaAsciiHex(h.agent)) content += _hotaTlv('8B', _hotaAsciiHex(h.agent));
|
||||||
return '89' + _hotaBerLen(content.length / 2) + content;
|
if (_hotaAsciiHex(h.uri)) content += _hotaTlv('8C', _hotaAsciiHex(h.uri));
|
||||||
|
return content ? '89' + _hotaBerLen(content.length / 2) + content : '';
|
||||||
}
|
}
|
||||||
function hotaBuildTrigger(conn, sec, retry, httpPost, expanded) {
|
function hotaBuildTrigger(conn, sec, retry, httpPost, expanded) {
|
||||||
let content = conn;
|
const content = (conn || '') + (sec || '') + (retry || '') + (httpPost || '');
|
||||||
if (sec) content += sec;
|
// '81' is mandatory but 0-n; '83' is 1-n, so an empty '83' is not emitted
|
||||||
if (retry) content += retry;
|
let trig = '8100';
|
||||||
if (httpPost) content += httpPost;
|
if (content) {
|
||||||
const sd = '83' + _hotaBerLen(content.length / 2) + content;
|
const sd = '83' + _hotaBerLen(content.length / 2) + content;
|
||||||
const trig = '81' + _hotaBerLen(sd.length / 2) + sd;
|
trig = '81' + _hotaBerLen(sd.length / 2) + sd;
|
||||||
|
}
|
||||||
if (expanded) return 'AA' + _hotaBerLen(trig.length / 2) + trig;
|
if (expanded) return 'AA' + _hotaBerLen(trig.length / 2) + trig;
|
||||||
return trig;
|
return trig;
|
||||||
}
|
}
|
||||||
function hotaBuildStore(conn, sec, retry, httpPost, storeTag) {
|
function hotaBuildStore(conn, sec, retry, httpPost, storeTag) {
|
||||||
const inner = _hotaTlv(storeTag, conn + (sec || '') + (retry || '') + (httpPost || ''));
|
const content = (conn || '') + (sec || '') + (retry || '') + (httpPost || '');
|
||||||
return '80E29000' + _hotaBerLen(inner.length / 2) + inner;
|
if (!content) return '';
|
||||||
|
const inner = _hotaTlv(storeTag, content);
|
||||||
|
const bytes = inner.length / 2;
|
||||||
|
if (bytes <= 127) return '80E29000' + _hotaBerLen(bytes) + inner;
|
||||||
|
// Parameters longer than a short APDU are chained as several STORE DATA
|
||||||
|
// commands: P1 b8=0 for all but the last block, P2 = block number,
|
||||||
|
// P1 b5-b4=10 (BER-TLV) throughout (GP Card Spec 11.11).
|
||||||
|
const chunks = [];
|
||||||
|
for (let i = 0; i < inner.length; i += 127 * 2) chunks.push(inner.slice(i, i + 127 * 2));
|
||||||
|
return chunks.map((c, i) => {
|
||||||
|
const last = i === chunks.length - 1;
|
||||||
|
const p2 = i.toString(16).padStart(2, '0').toUpperCase();
|
||||||
|
return '80E2' + (last ? '90' : '10') + p2 + _hotaBerLen(c.length / 2) + c;
|
||||||
|
}).join('');
|
||||||
}
|
}
|
||||||
function hotaBuild(mode, conn, sec, retry, httpPost, storeTag, expanded) {
|
function hotaBuild(mode, conn, sec, retry, httpPost, storeTag, expanded) {
|
||||||
return mode === 'store'
|
return mode === 'store'
|
||||||
@@ -1433,9 +1455,10 @@ function hotaAddPreset(name) {
|
|||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'flex gap-1 items-center mb-1';
|
row.className = 'flex gap-1 items-center mb-1';
|
||||||
const presets = {
|
const presets = {
|
||||||
devid: ['02', '8182'],
|
devid: ['82', '8182'],
|
||||||
alpha: ['80', ''],
|
alpha: ['05', ''],
|
||||||
bearer: ['01', '014001'],
|
cmddet: ['81', '014001'],
|
||||||
|
bearer: ['35', '03'],
|
||||||
};
|
};
|
||||||
const [tag, value] = presets[name] || ['', ''];
|
const [tag, value] = presets[name] || ['', ''];
|
||||||
row.innerHTML =
|
row.innerHTML =
|
||||||
@@ -11346,11 +11369,12 @@ const LANG_RU = {
|
|||||||
'+ TLV': '+ TLV',
|
'+ TLV': '+ TLV',
|
||||||
'+ Device Identities': '+ Device Identities',
|
'+ Device Identities': '+ Device Identities',
|
||||||
'+ Alpha (empty)': '+ Alpha (пустой)',
|
'+ Alpha (empty)': '+ Alpha (пустой)',
|
||||||
'+ Bearer (GPRS)': '+ Bearer (GPRS)',
|
'+ Command details': '+ Command details',
|
||||||
|
'+ Bearer (default)': '+ Bearer (по умолчанию)',
|
||||||
"Security parameters ('85')": "Параметры безопасности ('85')",
|
"Security parameters ('85')": "Параметры безопасности ('85')",
|
||||||
'PSK Identity (text)': 'PSK Identity (текст)',
|
'PSK Identity (text)': 'PSK Identity (текст)',
|
||||||
"Retry policy ('86')": "Политика повтора ('86')",
|
"Retry policy ('86')": "Политика повтора ('86')",
|
||||||
'Retry counter (B0,00=unlimited)': 'Счётчик повторов (B0,00=бесконечно)',
|
'Retry counter (2 bytes, hex)': 'Счётчик повторов (2 байта, hex)',
|
||||||
'Hours': 'Часы',
|
'Hours': 'Часы',
|
||||||
'Minutes': 'Минуты',
|
'Minutes': 'Минуты',
|
||||||
'Seconds': 'Секунды',
|
'Seconds': 'Секунды',
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'otaman-v176';
|
const CACHE = 'otaman-v177';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ function extractFunc(src, name) {
|
|||||||
return src.slice(m.index, i + 1);
|
return src.slice(m.index, i + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const FNS = ['_hotaHex', '_hotaAsciiHex', '_hotaBerLen', '_hotaTlv',
|
const FNS = ['_hotaHex', '_hotaAsciiHex', '_hotaBerLen', '_hotaPad', '_hotaTlv',
|
||||||
'hotaTlv', 'hotaBuildConn', 'hotaBuildSec', 'hotaBuildRetry',
|
'hotaTlv', 'hotaBuildConn', 'hotaBuildSec', 'hotaBuildRetry',
|
||||||
'hotaBuildHttpPost', 'hotaBuildTrigger', 'hotaBuildStore', 'hotaBuild'];
|
'hotaBuildHttpPost', 'hotaBuildTrigger', 'hotaBuildStore', 'hotaBuild'];
|
||||||
let code = '';
|
let code = '';
|
||||||
@@ -68,7 +68,10 @@ test('hotaBuildConn produces a full 84 TLV', () => {
|
|||||||
]),
|
]),
|
||||||
'840B0103014001020281820500');
|
'840B0103014001020281820500');
|
||||||
assert.strictEqual(hotaBuildConn([{ tag: '02', value: '8182' }, { tag: '', value: 'FF' }]), '840402028182');
|
assert.strictEqual(hotaBuildConn([{ tag: '02', value: '8182' }, { tag: '', value: 'FF' }]), '840402028182');
|
||||||
assert.strictEqual(hotaBuildConn([]), '8400');
|
// a half byte is padded, never turned into a fractional BER length
|
||||||
|
assert.strictEqual(hotaBuildConn([{ tag: '35', value: '3' }]), '8403350103');
|
||||||
|
// '84' is 1-n per Table 4-5: an empty container is left out entirely
|
||||||
|
assert.strictEqual(hotaBuildConn([]), '');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('hotaBuildSec produces a full 85 TLV per Table 4-6', () => {
|
test('hotaBuildSec produces a full 85 TLV per Table 4-6', () => {
|
||||||
@@ -81,21 +84,36 @@ test('hotaBuildSec produces a full 85 TLV per Table 4-6', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('hotaBuildRetry uses TS 102 223 timer TLV (25 03) and wraps in 86', () => {
|
test('hotaBuildRetry uses TS 102 223 timer TLV (25 03) and wraps in 86', () => {
|
||||||
|
// the delay fields use the TP-SCTS semi-octet order (TS 23.040 9.1.2.3):
|
||||||
|
// within an octet the low nibble holds the most significant digit, so
|
||||||
|
// 1 min = '10', 20 s = '02', 10 min = '01', 1 h 2 min 3 s = '10 20 30'
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
hotaBuildRetry({ counter: 'B000', delayH: 1, delayM: 2, delayS: 3, reportFailure: '' }),
|
hotaBuildRetry({ counter: 'B000', delayH: 1, delayM: 2, delayS: 3, reportFailure: '' }),
|
||||||
'8607B0002503010203');
|
'8607B0002503102030');
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
hotaBuildRetry({ counter: 'b0 00', delayH: '1', delayM: '2', delayS: '3', reportFailure: '0A080102030405060708' }),
|
hotaBuildRetry({ counter: 'b0 00', delayH: '1', delayM: '2', delayS: '3', reportFailure: '0A080102030405060708' }),
|
||||||
'8611B00025030102030A080102030405060708');
|
'8611B00025031020300A080102030405060708');
|
||||||
|
// counter is 2 bytes mandatory: short input is left-padded, odd report dropped
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
hotaBuildRetry({ counter: '0000', delayH: 0, delayM: 0, delayS: 0, reportFailure: '0A0' }),
|
hotaBuildRetry({ counter: '0000', delayH: 0, delayM: 0, delayS: 0, reportFailure: '0A0' }),
|
||||||
'860700002503000000');
|
'860700002503000000');
|
||||||
|
// round-trip of the card's own 1-minute timer value 25 03 00 10 00
|
||||||
|
assert.strictEqual(
|
||||||
|
hotaBuildRetry({ counter: '0001', delayH: 0, delayM: 1, delayS: 0, reportFailure: '' }),
|
||||||
|
'860700012503001000');
|
||||||
|
assert.strictEqual(
|
||||||
|
hotaBuildRetry({ counter: 'B0', delayH: 0, delayM: 0, delayS: 20, reportFailure: '' }),
|
||||||
|
'860700B02503000002');
|
||||||
|
assert.strictEqual(
|
||||||
|
hotaBuildRetry({ counter: '0', delayH: 0, delayM: 10, delayS: 0, reportFailure: '' }),
|
||||||
|
'860700002503000100');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('hotaBuildHttpPost wraps 8A/8B/8C in a full 89 TLV', () => {
|
test('hotaBuildHttpPost wraps non-empty 8A/8B/8C in a full 89 TLV', () => {
|
||||||
assert.strictEqual(
|
// '8A'..'8C' are 1-n per Tables 4-8/9/10: empty parameters are omitted
|
||||||
hotaBuildHttpPost({ host: '', agent: '', uri: '' }),
|
assert.strictEqual(hotaBuildHttpPost({ host: '', agent: '', uri: '' }), '');
|
||||||
'89068A008B008C00');
|
assert.strictEqual(hotaBuildHttpPost({ host: 'megafon.ru' }), '890C8A0A6D656761666F6E2E7275');
|
||||||
|
assert.strictEqual(hotaBuildHttpPost({ agent: 'v1.0' }), '89068B0476312E30');
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
hotaBuildHttpPost({ host: 'megafon.ru', agent: 'v1.0', uri: '/sd' }),
|
hotaBuildHttpPost({ host: 'megafon.ru', agent: 'v1.0', uri: '/sd' }),
|
||||||
'89178A0A6D656761666F6E2E72758B0476312E308C032F7364');
|
'89178A0A6D656761666F6E2E72758B0476312E308C032F7364');
|
||||||
@@ -104,11 +122,13 @@ test('hotaBuildHttpPost wraps 8A/8B/8C in a full 89 TLV', () => {
|
|||||||
test('hotaBuildTrigger wraps 81 > 83 > (84/85/86/89)', () => {
|
test('hotaBuildTrigger wraps 81 > 83 > (84/85/86/89)', () => {
|
||||||
const conn = '840402028182';
|
const conn = '840402028182';
|
||||||
const sec = '850400020101';
|
const sec = '850400020101';
|
||||||
const retry = '8607B0002503010203';
|
const retry = '8607B0002503102030';
|
||||||
const httpPost = '89068A008B008C00';
|
const httpPost = '890C8A0A6D656761666F6E2E7275';
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
hotaBuildTrigger(conn, sec, retry, httpPost, false),
|
hotaBuildTrigger(conn, sec, retry, httpPost, false),
|
||||||
'811F831D8404020281828504000201018607B000250301020389068A008B008C00');
|
'812583238404020281828504000201018607B0002503102030890C8A0A6D656761666F6E2E7275');
|
||||||
|
// '81' is mandatory but 0-n; without parameters it stays an empty container
|
||||||
|
assert.strictEqual(hotaBuildTrigger('', '', '', '', false), '8100');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('hotaBuildTrigger expanded wraps the 81 command in Command Scripting template AA', () => {
|
test('hotaBuildTrigger expanded wraps the 81 command in Command Scripting template AA', () => {
|
||||||
@@ -127,6 +147,19 @@ test('hotaBuildStore emits STORE DATA TLV-mode APDU (80 E2 90 00)', () => {
|
|||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
hotaBuildStore('840402028182', '', '', '', 'A5'),
|
hotaBuildStore('840402028182', '', '', '', 'A5'),
|
||||||
'80E2900008A506840402028182');
|
'80E2900008A506840402028182');
|
||||||
|
// nothing to store -> no APDU
|
||||||
|
assert.strictEqual(hotaBuildStore('', '', '', '', '85'), '');
|
||||||
|
// parameters longer than a short APDU are chained: P1.b8=0 for all but
|
||||||
|
// the last block, P2 = block number, BER-TLV coding throughout
|
||||||
|
const big = '84' + _hotaBerLen(140) + 'AA'.repeat(140);
|
||||||
|
const inner = '85' + _hotaBerLen(big.length / 2) + big;
|
||||||
|
const blocks = [];
|
||||||
|
for (let i = 0; i < inner.length; i += 127 * 2) blocks.push(inner.slice(i, i + 127 * 2));
|
||||||
|
assert.strictEqual(blocks.length, 2);
|
||||||
|
assert.strictEqual(
|
||||||
|
hotaBuildStore(big, '', '', '', '85'),
|
||||||
|
'80E210007F' + blocks[0] +
|
||||||
|
'80E29001' + (blocks[1].length / 2).toString(16).padStart(2, '0').toUpperCase() + blocks[1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('hotaBuild dispatches on mode', () => {
|
test('hotaBuild dispatches on mode', () => {
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pysim-otaman-server"
|
name = "pysim-otaman-server"
|
||||||
version = "2.2.11"
|
version = "2.2.12"
|
||||||
description = "HTTP REST server wrapping pysim for the OTAMan PWA"
|
description = "HTTP REST server wrapping pysim for the OTAMan PWA"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from osmocom.construct import GsmOrUcs2Adapter
|
|||||||
from osmocom.tlv import BER_TLV_IE
|
from osmocom.tlv import BER_TLV_IE
|
||||||
|
|
||||||
|
|
||||||
VERSION = '2.2.11'
|
VERSION = '2.2.12'
|
||||||
|
|
||||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user