From 181544b360abdfa314fa74ebfb1bd0fec3df45de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Thu, 10 Sep 2026 21:30:06 +0300 Subject: [PATCH] frontend: add HTTP OTA constructor (GP RAM-over-HTTP v1.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New 'HTTP OTA' sub-tab in the C-APDU constructor builds Remote Application Management over HTTP payloads (GPC v2.2 Amendment B v1.1 §4.7): - Trigger (Push SMS): 81 > 83 > 84/[85]/[86]/89, with optional Command Scripting template ('AA') wrap per TS 102 226 §5.2.1 - Store (SD admin params): STORE DATA TLV mode (80 E2 90 00), tag 85/A5 - Connection parameters: OPEN CHANNEL comprehension-TLV row editor + presets (Device Identities 02, Alpha 80, Bearer 01) - Security parameters (Table 4-6): LV PSK Identity + LV KVN/KID - Retry policy (Table 4-7): retry counter + TS 102 223 timer TLV (25 03) - HTTP POST (Tables 4-8/9/10): 8A host / 8B agent / 8C URI, text→octets - 'Pack into Secured packet' reuses packToSp for SPI/counter filling Pure builder functions covered by 12 new tests reproducing the byte layout from the spec tables. i18n entries, help.html/help-ru.html §2.7 added. SW cache bumped otaman-v49 → v50. --- frontend/help-ru.html | 23 ++- frontend/help.html | 23 ++- frontend/index.html | 308 ++++++++++++++++++++++++++++++++- frontend/sw.js | 2 +- frontend/tests/httpota.test.js | 142 +++++++++++++++ 5 files changed, 490 insertions(+), 8 deletions(-) create mode 100644 frontend/tests/httpota.test.js diff --git a/frontend/help-ru.html b/frontend/help-ru.html index 89b6a3e..3a31e6c 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -42,7 +42,7 @@

2. Вкладка C-APDU

-

Построение командных APDU (C-APDU). Пять подвкладок охватывают разные поколения карт и наборы команд: SIM RFM, USIM RFM, Expanded Script, RAM/GP и C-APDU Parser.

+

Построение командных APDU (C-APDU). Шесть подвкладок охватывают разные поколения карт и наборы команд: SIM RFM, USIM RFM, Expanded Script, RAM/GP, HTTP OTA и C-APDU Parser.

2.1 SIM RFM

CLA = A0 (GSM 11.11 / TS 151 011, ISO 7816-4). Удалённое управление файлами классических SIM-карт.

@@ -204,7 +204,26 @@

2.6 C-APDU Parser

-

Вставка raw APDU hex и отображение сворачиваемого дерева. Автоматически определяет контейнер: Expanded Script (начало AA или AE80, декодируется по ETSI TS 102 226 §5.2.1) или Compact C-APDU chain (последовательность C-APDU ISO 7816). Каждый узел показывает метку, hex и краткое описание; родительские узлы раскрываются в подэлементы. +

Вставка raw APDU hex и отображение сворачиваемого дерева. Автоматически определяет контейнер: Expanded Script (начало AA или AE80, декодируется по ETSI TS 102 226 §5.2.1) или Compact C-APDU chain (последовательность C-APDU ISO 7816). Каждый узел показывает метку, hex и краткое описание; родительские узлы раскрываются в подэлементы.

+ +

2.7 HTTP OTA

+

Сборка payload’ов Remote Application Management over HTTP по GlobalPlatform GPC v2.2 Amendment B v1.1 (§4.7). Два режима:

+ +

Разделы соответствуют таблицам спецификации:

+ + + + + + + + +
РазделTagСодержимое
Параметры соединения84Любые COMPREHENSION-TLV для открытия TCP-соединения (OPEN CHANNEL по TS 102 223): Device Identities 02, Alpha 80, Bearer 01, вендорские TLV. Редактор строк + пресеты, редактируемый hex.
Параметры безопасности85Таблица 4-6: LV PSK Identity (текст), LV Key version/KID. Идентифицирует ключ PSK TLS (RFC 4279).
Политика повтора86Таблица 4-7: счётчик повторов (2 байта, напр. B000), задержка повторной попытки как timer TLV из TS 102 223 (25 03 HH MM SS), опциональный вендорский TLV отчёта о сбое.
HTTP POST89Таблицы 4-8/9/10: Host-заголовок (8A), X-Admin-From agent ID (8B), URI (8C) — текст преобразуется в октеты.
+

Command Scripting template (флажок) оборачивает всю команду 81 в формат Expanded Remote Application data с определённой длиной (AA, ETSI TS 102 226 §5.2.1) для TAR-ов, обрабатывающих расширенный формат (RAM-over-HTTP §4.7).

+

Pack into Secured packet отправляет готовый payload на вкладку SCP80 для заполнения SPI/счётчика — там укажите TAR, который слушает SD (обычно TAR OTASD).

diff --git a/frontend/help.html b/frontend/help.html index 09aadad..1fe3a66 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -42,7 +42,7 @@

2. C-APDU tab

-

Builds command APDUs (C-APDUs). Five sub-tabs cover different card generations and command sets: SIM RFM, USIM RFM, Expanded Script, RAM/GP, and C-APDU Parser.

+

Builds command APDUs (C-APDUs). Six sub-tabs cover different card generations and command sets: SIM RFM, USIM RFM, Expanded Script, RAM/GP, HTTP OTA, and C-APDU Parser.

2.1 SIM RFM

CLA = A0 (GSM 11.11 / TS 151 011, ISO 7816-4). Remote File Management for classic SIM cards.

@@ -204,7 +204,26 @@

2.6 C-APDU Parser

-

Pastes raw APDU hex and renders a collapsible tree. It auto-detects the container: an Expanded Script (leading AA or AE80, decoded per ETSI TS 102 226 §5.2.1) or a Compact C-APDU chain (a sequence of ISO 7816 C-APDUs). Each node shows its label, hex and a short description; parent nodes expand to reveal their sub-elements. +

Pastes raw APDU hex and renders a collapsible tree. It auto-detects the container: an Expanded Script (leading AA or AE80, decoded per ETSI TS 102 226 §5.2.1) or a Compact C-APDU chain (a sequence of ISO 7816 C-APDUs). Each node shows its label, hex and a short description; parent nodes expand to reveal their sub-elements.

+ +

2.7 HTTP OTA

+

Builds the Remote Application Management over HTTP payloads defined in GlobalPlatform GPC v2.2 Amendment B v1.1 (§4.7). Two modes:

+
    +
  • Trigger (Push SMS) — administration session triggering parameters (81 > 83 > 84/[85]/[86]/89, Table 4-3). This is the message that asks the card's Security Domain to dial out and start an HTTP session.
  • +
  • Store (SD admin params) — writes the same parameters as card (Security Domain) data via STORE DATA in TLV mode (80 E2 90 00, P1=90 = last block + BER-TLV per GP v2.2 Amendment B v1.1.3), wrapped in tag 85 (or A5) per Table 4-4.
  • +
+

Sections mirror the spec tables:

+ + + + + + + + +
SectionTagContents
Connection parameters84Any COMPREHENSION-TLV needed to open the TCP connection (OPEN CHANNEL per TS 102 223): Device Identities 02, Alpha 80, Bearer 01, vendor TLVs. Row editor + presets, editable hex.
Security parameters85Table 4-6: LV PSK Identity (text), LV Key version/KID. Identifies the PSK TLS key (RFC 4279).
Retry policy86Table 4-7: retry counter (2 bytes, e.g. B000), retry waiting delay as the TS 102 223 timer TLV (25 03 HH MM SS), optional vendor-specific report-failure TLV.
HTTP POST89Tables 4-8/9/10: Host header (8A), X-Admin-From agent ID (8B), URI (8C) — text converted to octets.
+

Command Scripting template (checkbox) wraps the whole 81 triggering command in the definite-length Expanded Remote Application data format (AA, ETSI TS 102 226 §5.2.1) for TARs that process the expanded format (RAM-over-HTTP §4.7).

+

Pack into Secured packet sends the built payload to the SCP80 tab for SPI/counter filling — insert the TAR the SD listens on (typically the OTASD TAR) there.

diff --git a/frontend/index.html b/frontend/index.html index 46ffd83..36cec99 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -43,7 +43,8 @@ - + +
@@ -142,6 +143,119 @@
+ +
@@ -874,12 +988,178 @@ function cApduSwitchSubtab(name) { btn.classList.toggle('text-gray-700', !active); btn.classList.toggle('dark:text-slate-300', !active); }); - ['sim', 'usim', 'ber', 'ram', 'parse'].forEach(s => { + ['sim', 'usim', 'ber', 'ram', 'parse', 'httpota'].forEach(s => { const el = document.getElementById('c-apdu-sub-' + s); if (el) el.classList.toggle('hidden', s !== name); }); - setHelpAnchor({sim:'sim-rfm', usim:'usim-rfm', ber:'ber-tlv', ram:'ram-gp', parse:'c-apdu'}[name] || 'c-apdu'); + setHelpAnchor({sim:'sim-rfm', usim:'usim-rfm', ber:'ber-tlv', ram:'ram-gp', parse:'c-apdu', httpota:'http-ota'}[name] || 'c-apdu'); } + +// ===== HTTP OTA constructor ===== +let _hotaMode = 'trigger'; +function _hotaHex(s) { return (s || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); } +function _hotaAsciiHex(s) { return (s || '').split('').map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join('').toUpperCase(); } +function _hotaBerLen(n) { + if (n < 128) return 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(); +} +function _hotaTlv(tag8, val8) { const v = _hotaHex(val8); return tag8.toUpperCase() + _hotaBerLen(v.length / 2) + v; } + +function hotaTlv(tag, value) { return _hotaTlv(tag, value); } +function hotaBuildConn(tlvs) { + let content = ''; + for (const t of tlvs) { + const v = _hotaHex(t.value); + if (!t.tag) continue; + content += t.tag.toUpperCase() + _hotaBerLen(v.length / 2) + v; + } + return '84' + _hotaBerLen(content.length / 2) + content; +} +function hotaBuildSec(s) { + const psk = _hotaAsciiHex(s.pskIdentity); + const kvn = _hotaHex(s.kvn).padStart(2, '0'); + const kid = _hotaHex(s.kid).padStart(2, '0'); + const data = (psk.length / 2).toString(16).padStart(2, '0').toUpperCase() + psk + '02' + kvn + kid; + return '85' + _hotaBerLen(data.length / 2) + data; +} +function hotaBuildRetry(r) { + const counter = _hotaHex(r.counter); + const hh = (parseInt(r.delayH) || 0).toString(16).padStart(2, '0').toUpperCase(); + const mm = (parseInt(r.delayM) || 0).toString(16).padStart(2, '0').toUpperCase(); + const ss = (parseInt(r.delayS) || 0).toString(16).padStart(2, '0').toUpperCase(); + const timerTlv = '2503' + hh + mm + ss; + const report = _hotaHex(r.reportFailure); + const content = counter + timerTlv + (report && report.length % 2 === 0 ? report : ''); + return '86' + _hotaBerLen(content.length / 2) + content; +} +function hotaBuildHttpPost(h) { + const host = _hotaAsciiHex(h.host); + const agent = _hotaAsciiHex(h.agent); + const uri = _hotaAsciiHex(h.uri); + const content = _hotaTlv('8A', host) + _hotaTlv('8B', agent) + _hotaTlv('8C', uri); + return '89' + _hotaBerLen(content.length / 2) + content; +} +function hotaBuildTrigger(conn, sec, retry, httpPost, expanded) { + let content = conn; + if (sec) content += sec; + if (retry) content += retry; + if (httpPost) content += httpPost; + const sd = '83' + _hotaBerLen(content.length / 2) + content; + const trig = '81' + _hotaBerLen(sd.length / 2) + sd; + if (expanded) return 'AA' + _hotaBerLen(trig.length / 2) + trig; + return trig; +} +function hotaBuildStore(conn, sec, retry, httpPost, storeTag) { + const inner = _hotaTlv(storeTag, conn + (sec || '') + (retry || '') + (httpPost || '')); + return '80E29000' + _hotaBerLen(inner.length / 2) + inner; +} +function hotaBuild(mode, conn, sec, retry, httpPost, storeTag, expanded) { + return mode === 'store' + ? hotaBuildStore(conn, sec, retry, httpPost, storeTag) + : hotaBuildTrigger(conn, sec, retry, httpPost, expanded); +} + +function hotaSetMode(mode) { + _hotaMode = mode; + document.getElementById('hota-mode-trigger').className = 'px-4 py-1.5 text-sm rounded-full ' + (mode === 'trigger' ? 'bg-blue-600 text-white' : 'bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600'); + document.getElementById('hota-mode-store').className = 'px-4 py-1.5 text-sm rounded-full ' + (mode === 'store' ? 'bg-blue-600 text-white' : 'bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600'); + document.getElementById('hota-store-tag-row').classList.toggle('hidden', mode !== 'store'); + document.getElementById('hota-expanded-row').classList.toggle('hidden', mode !== 'trigger'); + hotaUpdatePreview(); +} + +function hotaToggleConn() { + const on = document.getElementById('hota-include-conn').checked; + document.getElementById('hota-conn-tlvs').classList.toggle('hidden', !on); + hotaUpdatePreview(); +} +document.addEventListener('change', function(e) { + if (e.target && e.target.id === 'hota-include-conn') hotaToggleConn(); + else if (e.target && e.target.id === 'hota-include-sec') document.getElementById('hota-sec-fields').classList.toggle('hidden', !e.target.checked); + else if (e.target && e.target.id === 'hota-include-retry') document.getElementById('hota-retry-fields').classList.toggle('hidden', !e.target.checked); + else if (e.target && e.target.id === 'hota-include-http') document.getElementById('hota-http-fields').classList.toggle('hidden', !e.target.checked); +}); +hotaToggleConn(); + +function hotaAddTlv() { + const container = document.getElementById('hota-conn-tlvs'); + const row = document.createElement('div'); + row.className = 'flex gap-1 items-center mb-1'; + row.innerHTML = + '' + + '' + + ''; + container.appendChild(row); + hotaUpdatePreview(); +} +function hotaAddPreset(name) { + const container = document.getElementById('hota-conn-tlvs'); + const row = document.createElement('div'); + row.className = 'flex gap-1 items-center mb-1'; + const presets = { + devid: ['02', '8182'], + alpha: ['80', ''], + bearer: ['01', '014001'], + }; + const [tag, value] = presets[name] || ['', '']; + row.innerHTML = + '' + + '' + + ''; + container.appendChild(row); + hotaUpdatePreview(); +} + +function hotaReadConnTlvs() { + const tlvs = []; + document.querySelectorAll('#hota-conn-tlvs .flex').forEach(row => { + const inputs = row.querySelectorAll('input'); + if (inputs.length >= 2) tlvs.push({ tag: _hotaHex(inputs[0].value), value: inputs[1].value }); + }); + return tlvs; +} + +function hotaUpdatePreview() { + const connOn = document.getElementById('hota-include-conn').checked; + const secOn = document.getElementById('hota-include-sec').checked; + const retryOn = document.getElementById('hota-include-retry').checked; + const httpOn = document.getElementById('hota-include-http').checked; + const expanded = document.getElementById('hota-expanded').checked; + const storeTag = document.getElementById('hota-store-tag').value; + const conn = connOn ? hotaBuildConn(hotaReadConnTlvs()) : ''; + const sec = secOn ? hotaBuildSec({ + pskIdentity: document.getElementById('hota-psk-text').value, + kvn: document.getElementById('hota-kvn').value, + kid: document.getElementById('hota-kid').value, + }) : ''; + const retry = retryOn ? hotaBuildRetry({ + counter: document.getElementById('hota-retry-counter').value, + delayH: document.getElementById('hota-delay-h').value, + delayM: document.getElementById('hota-delay-m').value, + delayS: document.getElementById('hota-delay-s').value, + reportFailure: document.getElementById('hota-retry-report').value, + }) : ''; + const http = httpOn ? hotaBuildHttpPost({ + host: document.getElementById('hota-host').value, + agent: document.getElementById('hota-agent').value, + uri: document.getElementById('hota-uri').value, + }) : ''; + const hex = hotaBuild(_hotaMode, conn, sec, retry, http, storeTag, expanded); + const preview = document.getElementById('hota-preview'); + preview.value = hex; + document.getElementById('hota-pack-btn').disabled = !hex; +} + +function hotaPreviewEdited() { + const hex = document.getElementById('hota-preview').value; + document.getElementById('hota-pack-btn').disabled = !hex; +} +document.querySelectorAll('#c-apdu-sub-httpota input, #c-apdu-sub-httpota select').forEach(el => { + if (el.type === 'checkbox') return; + el.addEventListener('input', hotaUpdatePreview); +}); + document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', () => switchTab(btn.dataset.tab)); }); @@ -7530,6 +7810,28 @@ const LANG_RU = { 'errors': 'ошибок', 'expected': 'ожидалось', 'actual': 'фактически', + 'HTTP OTA': 'HTTP OTA', + 'Trigger (Push SMS)': 'Триггер (Push SMS)', + 'Store (SD admin params)': 'Store (параметры SD)', + 'SD params tag': 'Tag параметров SD', + "Connection parameters ('84')": "Параметры соединения ('84')", + '+ TLV': '+ TLV', + '+ Device Identities': '+ Device Identities', + '+ Alpha (empty)': '+ Alpha (пустой)', + '+ Bearer (GPRS)': '+ Bearer (GPRS)', + "Security parameters ('85')": "Параметры безопасности ('85')", + 'PSK Identity (text)': 'PSK Identity (текст)', + "Retry policy ('86')": "Политика повтора ('86')", + 'Retry counter (B0,00=unlimited)': 'Счётчик повторов (B0,00=бесконечно)', + 'Hours': 'Часы', + 'Minutes': 'Минуты', + 'Seconds': 'Секунды', + "Report failure (vendor-specific TLV)": "Отчёт о сбое (специфичный TLV)", + "HTTP POST ('89', 8A=host,8B=agent,8C=URI)": "HTTP POST ('89', 8A=host,8B=agent,8C=URI)", + 'Host (text→hex)': 'Хост (текст→hex)', + "HTTP agent ID (text, X-Admin-From)": "ID HTTP агента (текст, X-Admin-From)", + 'URI (text→hex)': 'URI (текст→hex)', + "Wrapped in Command Scripting template ('AA')": "Обёртка в Command Scripting template ('AA')", }; let currentLang = 'en'; diff --git a/frontend/sw.js b/frontend/sw.js index 56c1176..90de8c1 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'otaman-v49'; +const CACHE = 'otaman-v50'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/httpota.test.js b/frontend/tests/httpota.test.js new file mode 100644 index 0000000..7e56ced --- /dev/null +++ b/frontend/tests/httpota.test.js @@ -0,0 +1,142 @@ +const { test } = require('node:test'); +const assert = require('node:assert'); +const fs = require('node:fs'); +const path = require('node:path'); + +const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8'); + +function extractFunc(src, name) { + const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{'); + const m = re.exec(src); + if (!m) throw new Error('function ' + name + ' not found'); + let i = m.index + m[0].length - 1; + let depth = 0; + for (; i < src.length; i++) { + if (src[i] === '{') depth++; + else if (src[i] === '}') { + depth--; + if (depth === 0) break; + } + } + return src.slice(m.index, i + 1); +} + +const FNS = ['_hotaHex', '_hotaAsciiHex', '_hotaBerLen', '_hotaTlv', + 'hotaTlv', 'hotaBuildConn', 'hotaBuildSec', 'hotaBuildRetry', + 'hotaBuildHttpPost', 'hotaBuildTrigger', 'hotaBuildStore', 'hotaBuild']; +let code = ''; +for (const f of FNS) code += extractFunc(html, f) + '\n'; + +eval(code); + +test('_hotaHex strips non-hex and uppercases', () => { + assert.strictEqual(_hotaHex(' 81 82 '), '8182'); + assert.strictEqual(_hotaHex('b0-00'), 'B000'); + assert.strictEqual(_hotaHex(''), ''); + assert.strictEqual(_hotaHex(undefined), ''); +}); + +test('_hotaAsciiHex encodes text as ASCII bytes', () => { + assert.strictEqual(_hotaAsciiHex('megafon.ru'), '6D656761666F6E2E7275'); + assert.strictEqual(_hotaAsciiHex('v1.0'), '76312E30'); + assert.strictEqual(_hotaAsciiHex('/sd'), '2F7364'); + assert.strictEqual(_hotaAsciiHex(''), ''); +}); + +test('_hotaBerLen encodes definite lengths', () => { + assert.strictEqual(_hotaBerLen(0), '00'); + assert.strictEqual(_hotaBerLen(11), '0B'); + assert.strictEqual(_hotaBerLen(127), '7F'); + assert.strictEqual(_hotaBerLen(128), '8180'); + assert.strictEqual(_hotaBerLen(255), '81FF'); + assert.strictEqual(_hotaBerLen(256), '820100'); +}); + +test('hotaTlv wraps tag + definite length + value', () => { + assert.strictEqual(hotaTlv('02', '8182'), '02028182'); + assert.strictEqual(hotaTlv('05', ''), '0500'); + assert.strictEqual(hotaTlv('8A', '6D656761666F6E2E7275'), '8A0A6D656761666F6E2E7275'); +}); + +test('hotaBuildConn produces a full 84 TLV', () => { + assert.strictEqual(hotaBuildConn([{ tag: '02', value: '8182' }]), '840402028182'); + assert.strictEqual( + hotaBuildConn([ + { tag: '01', value: '014001' }, + { tag: '02', value: '81 82' }, + { tag: '05', value: '' }, + ]), + '840B0103014001020281820500'); + assert.strictEqual(hotaBuildConn([{ tag: '02', value: '8182' }, { tag: '', value: 'FF' }]), '840402028182'); + assert.strictEqual(hotaBuildConn([]), '8400'); +}); + +test('hotaBuildSec produces a full 85 TLV per Table 4-6', () => { + assert.strictEqual( + hotaBuildSec({ pskIdentity: 'Test123', kvn: '01', kid: '01' }), + '850B0754657374313233020101'); + assert.strictEqual( + hotaBuildSec({ pskIdentity: '', kvn: '01', kid: '01' }), + '850400020101'); +}); + +test('hotaBuildRetry uses TS 102 223 timer TLV (25 03) and wraps in 86', () => { + assert.strictEqual( + hotaBuildRetry({ counter: 'B000', delayH: 1, delayM: 2, delayS: 3, reportFailure: '' }), + '8607B0002503010203'); + assert.strictEqual( + hotaBuildRetry({ counter: 'b0 00', delayH: '1', delayM: '2', delayS: '3', reportFailure: '0A080102030405060708' }), + '8611B00025030102030A080102030405060708'); + assert.strictEqual( + hotaBuildRetry({ counter: '0000', delayH: 0, delayM: 0, delayS: 0, reportFailure: '0A0' }), + '860700002503000000'); +}); + +test('hotaBuildHttpPost wraps 8A/8B/8C in a full 89 TLV', () => { + assert.strictEqual( + hotaBuildHttpPost({ host: '', agent: '', uri: '' }), + '89068A008B008C00'); + assert.strictEqual( + hotaBuildHttpPost({ host: 'megafon.ru', agent: 'v1.0', uri: '/sd' }), + '89178A0A6D656761666F6E2E72758B0476312E308C032F7364'); +}); + +test('hotaBuildTrigger wraps 81 > 83 > (84/85/86/89)', () => { + const conn = '840402028182'; + const sec = '850400020101'; + const retry = '8607B0002503010203'; + const httpPost = '89068A008B008C00'; + assert.strictEqual( + hotaBuildTrigger(conn, sec, retry, httpPost, false), + '811F831D8404020281828504000201018607B000250301020389068A008B008C00'); +}); + +test('hotaBuildTrigger expanded wraps the 81 command in Command Scripting template AA', () => { + const conn = '840402028182'; + const plain = hotaBuildTrigger(conn, '', '', '', false); + const expanded = hotaBuildTrigger(conn, '', '', '', true); + const byteLen = plain.length / 2; + assert.strictEqual(expanded, 'AA' + _hotaBerLen(byteLen) + plain); + assert.ok(expanded.startsWith('AA')); +}); + +test('hotaBuildStore emits STORE DATA TLV-mode APDU (80 E2 90 00)', () => { + assert.strictEqual( + hotaBuildStore('840402028182', '', '', '', '85'), + '80E29000088506840402028182'); + assert.strictEqual( + hotaBuildStore('840402028182', '', '', '', 'A5'), + '80E2900008A506840402028182'); +}); + +test('hotaBuild dispatches on mode', () => { + assert.strictEqual( + hotaBuild('trigger', '840402028182', '', '', '', '85', false), + hotaBuildTrigger('840402028182', '', '', '', false)); + assert.strictEqual( + hotaBuild('store', '840402028182', '', '', '', '85', false), + hotaBuildStore('840402028182', '', '', '', '85')); + assert.strictEqual( + hotaBuild('store', '840402028182', '', '8607B0002503010203', '', '85', false), + hotaBuildStore('840402028182', '', '8607B0002503010203', '', '85')); +}); \ No newline at end of file