diff --git a/frontend/help-ru.html b/frontend/help-ru.html index 6bf4913..0a9696e 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -55,7 +55,7 @@

2.1 SIM RFM

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

-

Команды собираются в виде цепочки: нажмите кнопку + SELECT (или другую командную кнопку), чтобы добавить строку, заполните её поля — предпросмотр цепочки (над кнопкой упаковки) обновится автоматически. Добавьте строку GET RESPONSE, чтобы получить данные после SELECT. Кнопка Упаковать в Secured packet упаковывает всю цепочку в пакет SCP80.

+

Команды собираются в виде цепочки: нажмите кнопку + SELECT (или другую командную кнопку), чтобы добавить строку, заполните её поля — предпросмотр цепочки (над кнопкой упаковки) обновится автоматически. Добавьте строку GET RESPONSE, чтобы получить данные после SELECT. Кнопка Упаковать в Secured packet упаковывает всю цепочку в пакет SCP80 и устанавливает TAR SCP80 в UICC TAR выбранной предустановки (по умолчанию B00000). Текущий DF сессии SIM RFM — MF, поэтому пути SELECT адресуют файлы от MF.

@@ -95,7 +95,8 @@

CLA = 00 (ETSI TS 102 221). Тот же сборщик цепочки и набор команд, что и SIM. Отличия:

@@ -275,7 +276,7 @@

Кнопка Проверить в pySim сверяет собранный пакет с эталонной реализацией OtaDialectSms.encode_cmd. Кнопка Отправить на карту доставляет пакет через ENVELOPE SMS-PP-DOWNLOAD (при подключении к серверу). Полученный Proof of Receipt декодируется и показывается строкой статуса PoR (статус, TAR, счётчик, сырой PoR); статусное слово и данные ответа последней команды подставляются в подвкладку «Парсер ответов» (Remote APDU), а успешный PoR увеличивает счётчик повторов и очищает пакет.

3.2 RAM

-

Выполняет операции удалённого управления приложениями (Remote Application Management) как защищённые пакеты SCP80 через SMS-PP-DOWNLOAD ENVELOPE. Карта должна поддерживать SCP03 (AES или 3DES). Предустановка карты со вкладки Карты обеспечивает SPI, ключи, TAR и счётчик.

+

Выполняет операции удалённого управления приложениями (Remote Application Management) как защищённые пакеты SCP80 через SMS-PP-DOWNLOAD ENVELOPE. Карта должна поддерживать SCP03 (AES или 3DES). Предустановка карты со вкладки Карты обеспечивает SPI, ключи, TAR и счётчик (RAM использует ISD TAR предустановки, по умолчанию 000000).

Операции

КомандаINSОписание
@@ -298,22 +299,25 @@

4. Вкладка «Карты»

-

Хранит предустановки карт локально в браузере (localStorage), чтобы представление Secured Packet могло автоматически подставлять ключи и параметры, а слушатель SCP81 HTTP OTA — находить PSK-ключи. «Карты» — верхнеуровневая вкладка. При подключении карты читается её EF.ICCID, и предустановка с тем же ICCID автоматически выбирается в обоих представлениях SCP80. Поля формы сгруппированы в два блока с рамкой: SCP80 (GSM 03.48, ETSI TS 102 225) (KIc/KID, SPI1/SPI2, TAR, счётчик и ключи) и SCP81 (HTTP OTA) (пара PSK identity/ключ с пояснением о выборе ключа).

+

Хранит предустановки карт локально в браузере (localStorage), чтобы представление Secured Packet могло автоматически подставлять ключи и параметры, а слушатель SCP81 HTTP OTA — находить PSK-ключи. «Карты» — верхнеуровневая вкладка. При подключении карты читается её EF.ICCID, и предустановка с тем же ICCID автоматически выбирается в обоих представлениях SCP80. Поля формы сгруппированы в два блока с рамкой: SCP80 (GSM 03.48, ETSI TS 102 225) (KIc/KID, SPI1/SPI2, TAR-ы ISD/UICC/USIM, счётчик и ключи) и SCP81 (HTTP OTA) (пара PSK identity/ключ с пояснением о выборе ключа). Необязательное поле ADM находится в верхней строке и сохраняется для последующего использования файловым менеджером.

+ - + + +
ПолеОписание
NameПонятная метка (обязательна)
ICCIDОпциональный идентификатор карты. Кнопка С карты подставляет EF.ICCID подключённой карты (активна, только когда ICCID читается); при сохранении ICCID, уже присутствующий в списке, отклоняется (сравнение без пробелов и с учётом сырой hex-формы, конфликтующий пресет называется)
ADMНеобязательный администраторский PIN (hex или до 8 ASCII-цифр), сохраняется в предустановке для последующего использования файловым менеджером; представлениями SCP80/SCP81 не используется
KIc / KIDИндикаторы ключа и алгоритма (например, 15 = индекс 1, 3DES-CBC2; x2 = AES); задаются вместе с ключами
SPI1 / SPI2Security Parameter Indicators
TARToolkit Application Reference
ISD TARToolkit Application Reference для операций ISD/RAM (представление SCP80 RAM, цепочки RAM/GP); по умолчанию по спецификации 000000, подставляется при пустом значении
UICC TARTAR для SIM RFM (TS 31.116); по умолчанию B00000 — сессия SIM RFM выбирает файлы от MF
USIM TARTAR для USIM RFM (TS 31.115); по умолчанию B00001 — текущий DF сессии USIM RFM — ADF.USIM
CounterСчётчик повторов (5 байт); увеличивается и сохраняется после каждой успешной отправки SCP80
KIc key / KID key16/24/32 hex-символа (ключи 8/16/24 байта 3DES) или 32/48/64 hex-символа (ключи 16/24/32 байта AES)
PSK identitySCP81 HTTP OTA: идентификатор, который карта присылает в TLS-рукопожатии (например, 89390100000129506903)
PSK keySCP81 HTTP OTA: 32 hex-символа (16 байт) для PSK TLS-слушателя; ключ выбирается по идентификатору, который предъявляет карта
-

Столбец SCP81 показывает, задана ли в предустановке рабочая пара PSK: (зелёный) — идентификатор и ключ заданы; (жёлтый) — задано только одно из двух, и слушатель такую предустановку игнорирует; — PSK нет. Идентификатор и ключ PSK задаются вместе. Кнопка Редактировать загружает предустановку в форму (кнопка Add становится Сохранить; Отмена очищает форму), поэтому поля можно менять без повторного ввода карты; Удалить удаляет строку. Обмен предустановками: Экспорт в файл для выгрузки и Импорт из файла для загрузки. Выбранная предустановка автоматически заполняет форму Secured Packet; изменения сразу передаются работающему слушателю SCP81.

+

Столбец SCP81 показывает, задана ли в предустановке рабочая пара PSK: (зелёный) — идентификатор и ключ заданы; (жёлтый) — задано только одно из двух, и слушатель такую предустановку игнорирует; — PSK нет. Идентификатор и ключ PSK задаются вместе. Кнопка Редактировать загружает предустановку в форму (кнопка Add становится Сохранить; Отмена очищает форму), поэтому поля можно менять без повторного ввода карты; Удалить удаляет строку. Обмен предустановками: Экспорт в файл для выгрузки и Импорт из файла для загрузки. Выбранная предустановка автоматически заполняет форму Secured Packet и устанавливает TAR для операции (RAM/GP → ISD TAR, SIM RFM → UICC TAR, USIM RFM → USIM TAR); изменения сразу передаются работающему слушателю SCP81.

diff --git a/frontend/help.html b/frontend/help.html index 55e28bd..18fd790 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -55,7 +55,7 @@

2.1 SIM RFM

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

-

Commands are assembled as a chain: press a command button (e.g. + SELECT) to append a row, fill that row’s fields, and the chain preview (above the pack button) updates automatically. Add a GET RESPONSE row to fetch data following a SELECT. Press Pack into Secured packet to wrap the whole chain into an SCP80 packet.

+

Commands are assembled as a chain: press a command button (e.g. + SELECT) to append a row, fill that row’s fields, and the chain preview (above the pack button) updates automatically. Add a GET RESPONSE row to fetch data following a SELECT. Press Pack into Secured packet to wrap the whole chain into an SCP80 packet; packing sets the SCP80 TAR to the selected preset’s UICC TAR (B00000 by default). A SIM RFM session’s current DF is MF, so SELECT paths address files from MF.

@@ -95,7 +95,8 @@

CLA = 00 (ETSI TS 102 221). Same chain builder and command set as SIM. Differences:

@@ -275,7 +276,7 @@

A “Verify vs pySim” button cross-checks the assembled packet against pySim’s reference OtaDialectSms.encode_cmd. A “Send to Card” button delivers it via SMS-PP-DOWNLOAD ENVELOPE (when connected to the server). The returned Proof of Receipt is decoded and shown as a PoR status line (status, TAR, counter, raw PoR); the last command’s status word and response data are filled into the Response parser tab, and a successful PoR advances the replay counter and clears the packet.

3.2 RAM

-

Delivers Remote Application Management operations as SCP80 secured packets via SMS-PP-DOWNLOAD ENVELOPE. The card must support SCP03 (AES or 3DES). A saved card preset from the Cards tab provides the SPI, keys, TAR, and counter.

+

Delivers Remote Application Management operations as SCP80 secured packets via SMS-PP-DOWNLOAD ENVELOPE. The card must support SCP03 (AES or 3DES). A saved card preset from the Cards tab provides the SPI, keys, TAR, and counter (RAM uses the preset's ISD TAR, 000000 by default).

Operations

CommandINSDescription
@@ -298,22 +299,25 @@

4. Cards tab

-

Stores card presets locally in the browser (localStorage) so the Secured Packet view can auto-fill keys and parameters, and so the SCP81 HTTP OTA listener can look up PSK keys. Cards is a top-level tab. When the card is equipped its EF.ICCID is read and the preset with the same ICCID is selected automatically in both SCP80 views. The form groups the fields into two bordered blocks: SCP80 (GSM 03.48, ETSI TS 102 225) (KIc/KID, SPI1/SPI2, TAR, counter and keys) and SCP81 (HTTP OTA) (the PSK identity/key pair with the key-selection note).

+

Stores card presets locally in the browser (localStorage) so the Secured Packet view can auto-fill keys and parameters, and so the SCP81 HTTP OTA listener can look up PSK keys. Cards is a top-level tab. When the card is equipped its EF.ICCID is read and the preset with the same ICCID is selected automatically in both SCP80 views. The form groups the fields into two bordered blocks: SCP80 (GSM 03.48, ETSI TS 102 225) (KIc/KID, SPI1/SPI2, the ISD/UICC/USIM TARs, counter and keys) and SCP81 (HTTP OTA) (the PSK identity/key pair with the key-selection note). The optional ADM field sits in the top row and is stored for later use by the file manager.

+ - + + +
FieldDescription
NameHuman-readable label (required)
ICCIDOptional card identifier. From card fills it with the equipped card's EF.ICCID (enabled only while a readable ICCID is known); saving refuses an ICCID that already exists in the list (compared ignoring spaces and the raw-hex form, and the conflicting preset is named)
ADMOptional administrator PIN (hex, or up to 8 ASCII digits), stored in the preset for later use by the file manager; not used by the SCP80/SCP81 views
KIc / KIDKey and algorithm indicators (e.g. 15 = index 1, 3DES-CBC2; x2 = AES), required together with the keys
SPI1 / SPI2Security Parameter Indicators
TARToolkit Application Reference
ISD TARToolkit Application Reference for ISD/RAM operations (SCP80 RAM view, RAM/GP chains); spec default 000000, prefilled when empty
UICC TARTAR for SIM RFM (TS 31.116); spec default B00000 — a SIM RFM session selects from MF
USIM TARTAR for USIM RFM (TS 31.115); spec default B00001 — a USIM RFM session's current DF is ADF.USIM
CounterReplay counter (5 bytes); advanced and saved after each successful SCP80 send
KIc key / KID key16/24/32 hex chars (8/16/24-byte 3DES) or 32/48/64 hex chars (16/24/32-byte AES) keys
PSK identitySCP81 HTTP OTA: the identity the card sends in the TLS handshake (e.g. 89390100000129506903)
PSK keySCP81 HTTP OTA: 32 hex chars (16 bytes) for the PSK TLS listener, picked by the identity the card presents
-

The SCP81 column shows whether the preset supplies a usable PSK pair: (green) — identity and key set; (amber) — only one of the two, so the listener ignores the preset; — no PSK. PSK identity and key must be set together. Edit loads a preset into the form (the Add button becomes Save; Cancel clears it) so fields can be changed without re-entering the card; Remove deletes the row. Presets can be shared with Export to file and restored with Import from file. The selected card preset auto-fills the Secured Packet form; edits are pushed into a running SCP81 listener automatically.

+

The SCP81 column shows whether the preset supplies a usable PSK pair: (green) — identity and key set; (amber) — only one of the two, so the listener ignores the preset; — no PSK. PSK identity and key must be set together. Edit loads a preset into the form (the Add button becomes Save; Cancel clears it) so fields can be changed without re-entering the card; Remove deletes the row. Presets can be shared with Export to file and restored with Import from file. The selected card preset auto-fills the Secured Packet form and sets the TAR for the operation (RAM/GP → ISD TAR, SIM RFM → UICC TAR, USIM RFM → USIM TAR); edits are pushed into a running SCP81 listener automatically.

diff --git a/frontend/index.html b/frontend/index.html index 391e075..09acb4b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1220,6 +1220,7 @@ + @@ -1230,7 +1231,9 @@ - + + +
@@ -1255,7 +1258,7 @@ KID SPI1 SPI2 - TAR + TAR (ISD/UICC/USIM) Cntr SCP81 @@ -1384,7 +1387,7 @@ // ===== Version ===== // Single source of truth for the PWA version: shown in the header and used // by the server version check in pysimConnect(). -const SIMPLE_VERSION = '2.5.2'; +const SIMPLE_VERSION = '2.6.0'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== @@ -2045,7 +2048,7 @@ function chainSimFieldsHtml(chainId, idx, f, uf) { ''; if (chainId === 'chain-usim') { html += '
' + - '
'; + '
'; } html += ''; } @@ -2855,12 +2858,36 @@ async function ramQueueScp81() { } } +// Which TAR of the selected preset feeds the SCP80 form: RAM/GP -> ISD, +// SIM RFM -> UICC, USIM RFM -> USIM (spec defaults when the value is empty). +let _spTarKey = 'tar'; + +function spTarKeyForPack(sourceId) { + return { + 'chain-ram-preview': 'tar', + 'chain-sim-preview': 'uiccTar', + 'chain-usim-preview': 'usimTar', + }[sourceId] || null; +} + +function spPresetTar(key) { + const sel = document.getElementById('sp-card-sel'); + const idx = sel ? parseInt(sel.value, 10) : NaN; + const c = (!isNaN(idx) && cards[idx]) ? cards[idx] : null; + return cardsTarValue(key, c ? c[key] : ''); +} + function packToSp(sourceId) { const val = document.getElementById(sourceId).value; if (!val) return; switchTab('scp80'); scp80SwitchSubtab('sp'); document.getElementById('sp-apdu').value = val; + const key = spTarKeyForPack(sourceId); + if (key) { + _spTarKey = key; + document.getElementById('sp-tar').value = spPresetTar(key); + } } function spCntrAdjust(delta) { const el = document.getElementById('sp-cntr'); @@ -5318,6 +5345,7 @@ function ramApplyCard(idx) { // getRamSpParams() picks up the right SPI/KIc/KID/TAR/CNTR/keys const i = parseInt(idx, 10); if (!isNaN(i) && cards[i]) _ramCardIdx = i; + _spTarKey = 'tar'; // RAM/GP operations target the ISD cardsApply(idx); } @@ -6087,6 +6115,13 @@ function cardsPskName(identity) { return c ? c.name : ''; } +// Spec-default TARs (GP ISD 000000, TS 31.116 UICC B00000, TS 31.115 USIM +// B00001): an empty preset value means the default for that target. +function cardsTarValue(key, value) { + const defaults = { tar: '000000', uiccTar: 'B00000', usimTar: 'B00001' }; + return value || defaults[key] || ''; +} + function cardsFormValues() { const val = id => { const el = document.getElementById('cards-' + id); @@ -6095,11 +6130,14 @@ function cardsFormValues() { return { name: val('name'), iccid: val('iccid'), + adm: val('adm').replace(/\s/g, '').toUpperCase(), kic: val('kic'), kid: val('kid'), spi1: val('spi1'), spi2: val('spi2'), tar: val('tar'), + uiccTar: val('uicc-tar'), + usimTar: val('usim-tar'), cntr: val('cntr'), kicKey: val('kic-key'), kidKey: val('kid-key'), @@ -6116,15 +6154,24 @@ function cardsSetFormMode() { } function cardsClearForm() { - ['name','iccid','kic','kid','spi1','spi2','tar','cntr','kic-key','kid-key', + ['name','iccid','adm','kic','kid','spi1','spi2','cntr','kic-key','kid-key', 'psk-id','psk-key'].forEach(id => { const el = document.getElementById('cards-' + id); if (el) el.value = ''; }); + // TARs keep their per-target spec defaults instead of being emptied + [['tar','tar'],['uicc-tar','uiccTar'],['usim-tar','usimTar']].forEach(([id, key]) => { + const el = document.getElementById('cards-' + id); + if (el) el.value = cardsTarValue(key, ''); + }); } function cardsAdd() { const v = cardsFormValues(); + // empty TAR fields fall back to the per-target spec defaults + v.tar = cardsTarValue('tar', v.tar); + v.uiccTar = cardsTarValue('uiccTar', v.uiccTar); + v.usimTar = cardsTarValue('usimTar', v.usimTar); if (!v.name) { alert(t('Name is required')); return; } if (!v.kic || !v.kid) { alert('KIc and KID are required'); return; } if (!v.kicKey || !v.kidKey) { alert('KIc key and KID key are required'); return; } @@ -6162,8 +6209,11 @@ function cardsEdit(i) { const el = document.getElementById('cards-' + id); if (el) el.value = value == null ? '' : value; }; - set('name', c.name); set('iccid', c.iccid); set('kic', c.kic); set('kid', c.kid); - set('spi1', c.spi1); set('spi2', c.spi2); set('tar', c.tar); set('cntr', c.cntr); + set('name', c.name); set('iccid', c.iccid); set('adm', c.adm); set('kic', c.kic); set('kid', c.kid); + set('spi1', c.spi1); set('spi2', c.spi2); set('cntr', c.cntr); + set('tar', cardsTarValue('tar', c.tar)); + set('uicc-tar', cardsTarValue('uiccTar', c.uiccTar)); + set('usim-tar', cardsTarValue('usimTar', c.usimTar)); set('kic-key', c.kicKey); set('kid-key', c.kidKey); set('psk-id', c.pskIdentity); set('psk-key', c.pskKey); cardsSetFormMode(); @@ -6213,7 +6263,7 @@ function cardsRender() { html += '' + esc(c.kid) + ''; html += '' + esc(c.spi1) + ''; html += '' + esc(c.spi2) + ''; - html += '' + esc(c.tar || '') + ''; + html += '' + esc(cardsTarValue('tar', c.tar)) + ' ' + esc(cardsTarValue('uiccTar', c.uiccTar)) + ' ' + esc(cardsTarValue('usimTar', c.usimTar)) + ''; html += '' + esc(c.cntr) + ''; html += '' + ota + ''; html += ' ' @@ -6306,7 +6356,7 @@ function cardsApply(idx) { document.getElementById('sp-kid-idx').value = ((kidByte >> 4) & 0x0F).toString(16).toUpperCase(); document.getElementById('sp-kid-alg').value = (kidByte & 0x0F).toString(16).padStart(2, '0'); updateSpKid(); - document.getElementById('sp-tar').value = c.tar || 'B00001'; + document.getElementById('sp-tar').value = cardsTarValue(_spTarKey, c[_spTarKey]); document.getElementById('sp-cntr').value = c.cntr; document.getElementById('sp-kic-key').value = c.kicKey; document.getElementById('sp-kid-key').value = c.kidKey; @@ -6349,11 +6399,14 @@ function cardsImport(text) { cards.push({ name: entry.name, iccid: entry.iccid, + adm: (entry.adm || '').replace(/\s/g, '').toUpperCase(), kic: entry.kic || '15', kid: entry.kid || '15', spi1: entry.spi1 || '16', spi2: entry.spi2 || '01', - tar: entry.tar || '', + tar: cardsTarValue('tar', entry.tar), + uiccTar: cardsTarValue('uiccTar', entry.uiccTar), + usimTar: cardsTarValue('usimTar', entry.usimTar), cntr: entry.cntr || '0000000001', kicKey: entry.kicKey || '', kidKey: entry.kidKey || '', diff --git a/frontend/sw.js b/frontend/sw.js index 244fc8d..e9eae51 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v198'; +const CACHE = 'simple-v199'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/cards_form.test.js b/frontend/tests/cards_form.test.js new file mode 100644 index 0000000..f484200 --- /dev/null +++ b/frontend/tests/cards_form.test.js @@ -0,0 +1,257 @@ +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); +} + +let code = ''; +for (const fn of ['cardsTarValue', 'cardsFormValues', 'cardsClearForm', 'cardsEdit', + 'cardsAdd', 'cardsImport', 'cardsApply', 'ramApplyCard', + 'spTarKeyForPack', 'spPresetTar', 'packToSp']) { + code += extractFunc(html, fn) + '\n'; +} +eval(code); + +const CARD_IDS = ['cards-name','cards-iccid','cards-adm','cards-kic','cards-kid', + 'cards-spi1','cards-spi2','cards-tar','cards-uicc-tar','cards-usim-tar', + 'cards-cntr','cards-kic-key','cards-kid-key','cards-psk-id','cards-psk-key', + 'cards-add-btn','cards-cancel-btn']; +const SP_IDS = ['sp-card-sel','sp-tar','sp-spi1','sp-spi2','sp-spi2-sm','sp-spi2-hex', + 'sp-kic-idx','sp-kic-alg','sp-kic-hex','sp-kid-idx','sp-kid-alg','sp-kid-hex', + 'sp-cntr','sp-kic-key','sp-kid-key','sp-apdu','sp-result','sp-spi1-hex']; +const CHAIN_IDS = ['chain-sim-preview','chain-usim-preview','chain-ram-preview', + 'ber-result','hota-preview']; + +function fakeEl() { + return { + value: '', + textContent: '', + innerHTML: '', + focus() {}, + classList: { add() {}, remove() {}, toggle() {}, contains: () => false }, + setAttribute() {}, removeAttribute() {}, + }; +} + +function setup() { + const els = {}; + for (const id of CARD_IDS.concat(SP_IDS, CHAIN_IDS)) els[id] = fakeEl(); + globalThis.document = { getElementById: id => els[id] || null, querySelectorAll: () => [] }; + globalThis.cards = []; + globalThis._cardsEditIdx = null; + globalThis._spTarKey = 'tar'; + globalThis.t = s => s; + globalThis.alert = () => {}; + globalThis.cardsSave = () => {}; + globalThis.cardsRender = () => {}; + globalThis.cardsRebuildSelect = () => {}; + globalThis.scp81PushPskMap = () => {}; + globalThis.cardsSetFormMode = () => {}; + globalThis.cardsCancelEdit = () => {}; + globalThis.cardsFindDuplicateIccid = () => -1; + globalThis.ioStatus = () => {}; + globalThis.switchTab = () => {}; + globalThis.scp80SwitchSubtab = () => {}; + globalThis.updateSpKic = () => {}; + globalThis.updateSpKid = () => {}; + globalThis.spInvalidate = () => {}; + globalThis.updateSp = () => {}; + globalThis.genSp = () => {}; + return els; +} + +test('cardsTarValue falls back to the per-target spec defaults', () => { + assert.strictEqual(cardsTarValue('tar', '123456'), '123456'); + assert.strictEqual(cardsTarValue('tar', ''), '000000'); + assert.strictEqual(cardsTarValue('uiccTar', ''), 'B00000'); + assert.strictEqual(cardsTarValue('usimTar', null), 'B00001'); + assert.strictEqual(cardsTarValue('nope', ''), ''); +}); + +test('clearing the card form keeps the TAR spec defaults', () => { + const els = setup(); + els['cards-tar'].value = 'AABBCC'; + els['cards-uicc-tar'].value = 'AABBCC'; + els['cards-usim-tar'].value = 'AABBCC'; + els['cards-adm'].value = 'DEAD'; + els['cards-name'].value = 'X'; + cardsClearForm(); + assert.strictEqual(els['cards-tar'].value, '000000'); + assert.strictEqual(els['cards-uicc-tar'].value, 'B00000'); + assert.strictEqual(els['cards-usim-tar'].value, 'B00001'); + assert.strictEqual(els['cards-adm'].value, ''); + assert.strictEqual(els['cards-name'].value, ''); +}); + +test('editing prefills empty stored TARs with the spec defaults', () => { + const els = setup(); + globalThis.cards = [ + { name: 'Legacy', kic: '15', kid: '15', tar: '' }, + { name: 'Full', kic: '15', kid: '15', tar: '000001', uiccTar: 'B0000C', usimTar: 'B0000D', adm: '0011' }, + ]; + cardsEdit(0); + assert.strictEqual(els['cards-tar'].value, '000000'); + assert.strictEqual(els['cards-uicc-tar'].value, 'B00000'); + assert.strictEqual(els['cards-usim-tar'].value, 'B00001'); + assert.strictEqual(els['cards-adm'].value, ''); + cardsEdit(1); + assert.strictEqual(els['cards-tar'].value, '000001'); + assert.strictEqual(els['cards-uicc-tar'].value, 'B0000C'); + assert.strictEqual(els['cards-usim-tar'].value, 'B0000D'); + assert.strictEqual(els['cards-adm'].value, '0011'); +}); + +test('the form values carry the ADM and per-target TAR fields', () => { + const els = setup(); + els['cards-tar'].value = ' 000001 '; + els['cards-uicc-tar'].value = 'B00002'; + els['cards-usim-tar'].value = 'B00003'; + els['cards-adm'].value = ' 00 11 22 33 '; + const v = cardsFormValues(); + assert.strictEqual(v.tar, '000001'); + assert.strictEqual(v.uiccTar, 'B00002'); + assert.strictEqual(v.usimTar, 'B00003'); + assert.strictEqual(v.adm, '00112233'); +}); + +test('saving a preset stores spec defaults for empty TARs', () => { + const els = setup(); + els['cards-name'].value = 'New card'; + els['cards-kic'].value = '15'; + els['cards-kid'].value = '15'; + els['cards-kic-key'].value = 'AA'; + els['cards-kid-key'].value = 'BB'; + els['cards-tar'].value = ''; + els['cards-uicc-tar'].value = ''; + els['cards-usim-tar'].value = ''; + els['cards-adm'].value = 'aa bb'; + cardsAdd(); + assert.strictEqual(cards.length, 1); + assert.strictEqual(cards[0].tar, '000000'); + assert.strictEqual(cards[0].uiccTar, 'B00000'); + assert.strictEqual(cards[0].usimTar, 'B00001'); + assert.strictEqual(cards[0].adm, 'AABB'); + assert.strictEqual(cards[0].spi1, '16'); +}); + +test('import fills missing TARs with spec defaults and normalizes ADM', () => { + setup(); + cardsImport(JSON.stringify([ + { name: 'Imported', kic: '15', kid: '15', adm: ' aa bb ' }, + { name: 'Explicit', tar: '000009', uiccTar: 'B0000C', usimTar: 'B0000D', adm: 'cc' }, + ])); + assert.strictEqual(cards.length, 2); + assert.strictEqual(cards[0].tar, '000000'); + assert.strictEqual(cards[0].uiccTar, 'B00000'); + assert.strictEqual(cards[0].usimTar, 'B00001'); + assert.strictEqual(cards[0].adm, 'AABB'); + assert.strictEqual(cards[1].tar, '000009'); + assert.strictEqual(cards[1].uiccTar, 'B0000C'); + assert.strictEqual(cards[1].usimTar, 'B0000D'); + assert.strictEqual(cards[1].adm, 'CC'); +}); + +test('applying a preset uses the TAR of the current operation', () => { + const els = setup(); + globalThis.cards = [{ + name: 'C', kic: '15', kid: '15', spi1: '16', spi2: '01', + tar: '000000', uiccTar: 'B0000C', usimTar: 'B0000D', + cntr: '0000000001', kicKey: 'AA', kidKey: 'BB', + }]; + globalThis._spTarKey = 'tar'; + cardsApply(0); + assert.strictEqual(els['sp-tar'].value, '000000'); + globalThis._spTarKey = 'uiccTar'; + cardsApply(0); + assert.strictEqual(els['sp-tar'].value, 'B0000C'); + globalThis._spTarKey = 'usimTar'; + cardsApply(0); + assert.strictEqual(els['sp-tar'].value, 'B0000D'); + // empty stored TAR -> spec default + globalThis.cards = [{ name: 'D', kic: '15', kid: '15', tar: '' }]; + globalThis._spTarKey = 'tar'; + cardsApply(0); + assert.strictEqual(els['sp-tar'].value, '000000'); +}); + +test('RAM card selection forces the ISD TAR', () => { + const els = setup(); + globalThis.cards = [{ name: 'C', kic: '15', kid: '15', tar: '000000', uiccTar: 'B0000C', usimTar: 'B0000D' }]; + globalThis._spTarKey = 'usimTar'; + ramApplyCard('0'); + assert.strictEqual(globalThis._spTarKey, 'tar'); + assert.strictEqual(els['sp-tar'].value, '000000'); +}); + +test('spTarKeyForPack maps each chain to its preset TAR field', () => { + assert.strictEqual(spTarKeyForPack('chain-ram-preview'), 'tar'); + assert.strictEqual(spTarKeyForPack('chain-sim-preview'), 'uiccTar'); + assert.strictEqual(spTarKeyForPack('chain-usim-preview'), 'usimTar'); + assert.strictEqual(spTarKeyForPack('ber-result'), null); + assert.strictEqual(spTarKeyForPack('hota-preview'), null); +}); + +test('spPresetTar reads the selected preset and falls back to the spec defaults', () => { + const els = setup(); + globalThis.cards = [{ name: 'A', tar: '000000', uiccTar: 'B0000A' }]; + els['sp-card-sel'].value = '0'; + assert.strictEqual(spPresetTar('uiccTar'), 'B0000A'); + assert.strictEqual(spPresetTar('usimTar'), 'B00001'); + els['sp-card-sel'].value = ''; + assert.strictEqual(spPresetTar('uiccTar'), 'B00000'); +}); + +test('packing a chain selects the TAR of its target', () => { + const els = setup(); + globalThis.cards = [{ name: 'C', tar: '000000', uiccTar: 'B0000C', usimTar: 'B0000D' }]; + els['sp-card-sel'].value = '0'; + els['chain-sim-preview'].value = 'A0A4000002'; + packToSp('chain-sim-preview'); + assert.strictEqual(globalThis._spTarKey, 'uiccTar'); + assert.strictEqual(els['sp-tar'].value, 'B0000C'); + assert.strictEqual(els['sp-apdu'].value, 'A0A4000002'); + els['chain-usim-preview'].value = '00A4000002'; + packToSp('chain-usim-preview'); + assert.strictEqual(globalThis._spTarKey, 'usimTar'); + assert.strictEqual(els['sp-tar'].value, 'B0000D'); + els['chain-ram-preview'].value = '80E29000'; + packToSp('chain-ram-preview'); + assert.strictEqual(globalThis._spTarKey, 'tar'); + assert.strictEqual(els['sp-tar'].value, '000000'); + // BER / HTTP OTA packs leave the TAR alone + els['sp-tar'].value = 'ABCDEF'; + els['ber-result'].value = 'AA0100'; + packToSp('ber-result'); + assert.strictEqual(els['sp-tar'].value, 'ABCDEF'); + assert.strictEqual(els['sp-apdu'].value, 'AA0100'); +}); + +test('packing without a selected preset uses the spec default TAR', () => { + const els = setup(); + els['chain-sim-preview'].value = 'A0A4000002'; + packToSp('chain-sim-preview'); + assert.strictEqual(els['sp-tar'].value, 'B00000'); + els['chain-usim-preview'].value = '00A4000002'; + packToSp('chain-usim-preview'); + assert.strictEqual(els['sp-tar'].value, 'B00001'); + els['chain-ram-preview'].value = '80E29000'; + packToSp('chain-ram-preview'); + assert.strictEqual(els['sp-tar'].value, '000000'); +}); diff --git a/frontend/tests/html.test.js b/frontend/tests/html.test.js index f774a68..a615595 100644 --- a/frontend/tests/html.test.js +++ b/frontend/tests/html.test.js @@ -33,6 +33,9 @@ test('the card form groups the SCP80 and SCP81 fields into labelled fieldsets', assert.ok(scp81, 'SCP81 card fieldset not found'); assert.match(scp80, /id="cards-kic"/); assert.match(scp80, /id="cards-kid-key"/); + assert.match(scp80, /id="cards-tar"[^>]*value="000000"/); + assert.match(scp80, /id="cards-uicc-tar"[^>]*value="B00000"/); + assert.match(scp80, /id="cards-usim-tar"[^>]*value="B00001"/); assert.match(scp81, /id="cards-psk-id"/); assert.match(scp81, /id="cards-psk-key"/); // the PSK explanation lives inside the SCP81 group, not outside it @@ -41,6 +44,17 @@ test('the card form groups the SCP80 and SCP81 fields into labelled fieldsets', assert.ok(!fieldsets.some(f => f.includes('id="cards-iccid"'))); }); +test('the ADM field sits between From card and Add, outside the card fieldsets', () => { + const fieldsets = [...html.matchAll(//g)].map(m => m[0]); + assert.ok(!fieldsets.some(f => f.includes('id="cards-adm"'))); + assert.match(html, /id="cards-adm"[^>]*placeholder="ADM \(optional\)"/); + const fromIdx = html.indexOf('id="cards-iccid-from-card"'); + const admIdx = html.indexOf('id="cards-adm"'); + const addIdx = html.indexOf('id="cards-add-btn"'); + assert.ok(fromIdx >= 0 && fromIdx < admIdx && admIdx < addIdx, + 'ADM input must sit between the From card and Add buttons'); +}); + test('labelled containers use fieldsets with embedded legends', () => { const legendCls = '