diff --git a/frontend/help-ru.html b/frontend/help-ru.html
index 99c3812..53109b8 100644
--- a/frontend/help-ru.html
+++ b/frontend/help-ru.html
@@ -254,7 +254,7 @@
- Открытие BIP-канала (
01) — собирается в режиме Fields из самых употребительных параметров OPEN CHANNEL (Stepping Stones §18.6.1): описание bearer, транспортный протокол (UDP или TCP, клиент UICC, удалённое соединение — только эти значения допускает команда) с портом назначения, адрес назначения, NAA/APN, размер буфера и alpha (флажок прозрачный режим отправляет нулевой alpha, т.е. без подтверждения пользователя); логин/пароль и остальное — в поле дополнительных TLV. Локальный адрес не отправляется никогда (§9.2.1 и руководство это запрещают). Режим Hex принимает сырые COMPREHENSION-TLV. Проактивную команду OPEN CHANNEL выдаёт само приложение (§9.1.1). SIMalliance Stepping Stones R7 §18.6 рекомендует отправлять его вместе с CAT_TP-запросом в одном сообщении (параметры канала — в 01, порт CAT_TP — в 02), поэтому в форме есть флажок «Также запросить установление соединения CAT_TP (02)»; пара передаётся одним скриптом команд (конкатенированные SMS, если больше 140 октетов).
- Установление соединения CAT_TP (
02) — порт назначения в transport level 3C с типом протокола 00 (обязателен), необязательные buffer size 39 / данные идентификации 36. Отдельно он полагается на предустановленные на карте умолчания канала/CAT_TP, против чего предостерегает руководство Stepping Stones; предпочтительна пара выше.
@@ -357,7 +357,7 @@
| PSK key | SCP81 HTTP OTA: 32 hex-символа (16 байт) для PSK TLS-слушателя; ключ выбирается по идентификатору, который предъявляет карта |
- Столбец SCP81 показывает, задана ли в предустановке рабочая пара PSK: ✓ (зелёный) — идентификатор и ключ заданы; ⚠ (жёлтый) — задано только одно из двух, и слушатель такую предустановку игнорирует; — — PSK нет. Идентификатор и ключ PSK задаются вместе. Кнопка Редактировать загружает предустановку в форму (кнопка Add становится Сохранить; Отмена очищает форму), поэтому поля можно менять без повторного ввода карты; Удалить удаляет строку. Обмен предустановками: Экспорт в файл для выгрузки и Импорт из файла для загрузки. Выбранная предустановка автоматически заполняет форму Secured Packet и устанавливает TAR для операции (RAM/GP → ISD TAR, SIM RFM → UICC RFM TAR, USIM RFM → ADF RFM TAR); изменения сразу передаются работающему слушателю SCP81.
+ Столбец SCP81 показывает, задана ли в предустановке рабочая пара PSK: ✓ (зелёный) — идентификатор и ключ заданы; ⚠ (жёлтый) — задано только одно из двух, и слушатель такую предустановку игнорирует; — — PSK нет. Идентификатор и ключ PSK задаются вместе. Кнопка Редактировать загружает предустановку в форму (кнопка Add становится Сохранить; Отмена очищает форму), поэтому поля можно менять без повторного ввода карты; Удалить удаляет строку. Обмен предустановками: Экспорт в файл для выгрузки и Импорт из файла для загрузки. Выбранная предустановка автоматически заполняет форму Secured Packet и устанавливает TAR для операции (RAM/GP → ISD TAR, SIM RFM → UICC RFM TAR, USIM RFM → ADF RFM TAR); TAR, введённый в форму Secured Packet вручную, сохраняется при отправке - счётчик, ключи и SPI по-прежнему берутся из предустановки; изменения сразу передаются работающему слушателю SCP81.
diff --git a/frontend/help.html b/frontend/help.html
index ef9cdce..2a52742 100644
--- a/frontend/help.html
+++ b/frontend/help.html
@@ -253,7 +253,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.
TS 102 226 §9 PUSH
- The Channel / link trigger sub-pill builds the §9 requests with the same encoder as the RAM/GP chain's PUSH row, previews the C-APDU(s), and offers Pack into Secured packet (the TAR stays manual: the request is addressed to the target application, not the Security Domain) and → Expanded Script (wraps each APDU in the 22 Command TLV and appends them to the Expanded Script builder).
+ The Channel / link trigger sub-pill builds the §9 requests with the same encoder as the RAM/GP chain's PUSH row, previews the C-APDU(s), and offers Pack into Secured packet (the TAR stays manual: the request is addressed to the target application, not the Security Domain, and a typed TAR is kept when sending) and → Expanded Script (wraps each APDU in the 22 Command TLV and appends them to the Expanded Script builder).
- BIP channel opening (
01) — built in Fields mode from the most-used OPEN CHANNEL parameters (Stepping Stones §18.6.1): bearer description, transport protocol (UDP or TCP, UICC client, remote — the only values the command allows) with the destination port, destination address, NAA/APN, buffer size and alpha (a transparent checkbox sends the null alpha, i.e. no user confirmation); login/password and anything else go in the extra-TLVs field. The local address is never emitted (§9.2.1 and the guide both forbid it). Hex mode takes the raw COMPREHENSION-TLVs instead. The application issues the proactive OPEN CHANNEL itself (§9.1.1). SIMalliance Stepping Stones R7 §18.6 recommends sending it together with the CAT_TP request in the same message (the channel parameters ride in 01, the CAT_TP port in 02), so the form has an Also request the CAT_TP link establishment (02) checkbox; the pair is one concatenated command script (concatenated SMS when it exceeds 140 octets).
- CAT_TP link establishment (
02) — destination port in transport level 3C with protocol type 00 (mandatory), optional buffer size 39 / identification data 36. Standalone it relies on card-provisioned channel/CAT_TP defaults, which the Stepping Stones guide advises against; prefer the pair above.
@@ -356,7 +356,7 @@
| PSK key | SCP81 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 and sets the TAR for the operation (RAM/GP → ISD TAR, SIM RFM → UICC RFM TAR, USIM RFM → ADF RFM TAR); 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 RFM TAR, USIM RFM → ADF RFM TAR); a TAR typed into the Secured Packet form by hand is kept when sending - the counter, keys and SPI are still re-read from the preset; edits are pushed into a running SCP81 listener automatically.
diff --git a/frontend/index.html b/frontend/index.html
index 5b817e5..cc59396 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1711,7 +1711,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 = '3.6.20';
+const SIMPLE_VERSION = '3.6.21';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
@@ -4869,7 +4869,7 @@ function _genSpBuild() {
const spi2 = parseInt(document.getElementById('sp-spi2-hex').value, 16);
const kicHex = document.getElementById('sp-kic-hex').value;
const kidHex = document.getElementById('sp-kid-hex').value;
- const tarHex = (document.getElementById('sp-tar').value || '').replace(/[^0-9a-fA-F]/g, '').padEnd(6, '0').slice(0, 6);
+ const tarHex = (document.getElementById('sp-tar').value || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase().padEnd(6, '0').slice(0, 6);
const cntrHex = (document.getElementById('sp-cntr').value || '').replace(/[^0-9a-fA-F]/g, '').padStart(10, '0').slice(0, 10);
const kicKeyHex = (document.getElementById('sp-kic-key').value || '').replace(/[^0-9a-fA-F]/g, '');
const kidKeyHex = (document.getElementById('sp-kid-key').value || '').replace(/[^0-9a-fA-F]/g, '');
@@ -8037,7 +8037,7 @@ function spParams() {
spi2: document.getElementById('sp-spi2-hex').value,
kic: document.getElementById('sp-kic-hex').value,
kid: document.getElementById('sp-kid-hex').value,
- tar: document.getElementById('sp-tar').value,
+ tar: (document.getElementById('sp-tar').value || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase().padEnd(6, '0').slice(0, 6),
cntr: document.getElementById('sp-cntr').value,
kicKey: document.getElementById('sp-kic-key').value,
kidKey: document.getElementById('sp-kid-key').value,
@@ -8078,10 +8078,13 @@ async function pysimSendOta() {
alert('Card reader server is not connected');
return;
}
- // Re-read the preset (counter and keys) before sending: the Cards tab may
- // have changed it after the form was filled, and a stale counter is
+ // Re-read the preset (counter, keys, SPI) before sending: the Cards tab
+ // may have changed it after the form was filled, and a stale counter is
// rejected by the card (cntr_low) and then written back over the preset.
- spRefreshFromPreset('sp-card-sel');
+ // The TAR is not re-applied: a packet may target a manually entered TAR
+ // (push/link triggers, applet OTA) - the preset sets it only when a card
+ // is picked or a chain is packed (v3.6.21).
+ spRefreshFromPreset('sp-card-sel', true);
const sp = document.getElementById('sp-result').value.replace(/[^0-9a-fA-F]/g, '').toUpperCase();
if (!sp) { alert('No secured packet to send. Generate a secure packet first.'); return; }
const sendResultEl = document.getElementById('sp-send-result');
@@ -8476,7 +8479,7 @@ function getRamSpParams() {
spi2: document.getElementById('sp-spi2-hex').value,
kic: document.getElementById('sp-kic-hex').value,
kid: document.getElementById('sp-kid-hex').value,
- tar: (document.getElementById('sp-tar').value || '000000').replace(/[^0-9a-fA-F]/g, ''),
+ tar: (document.getElementById('sp-tar').value || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase().padEnd(6, '0').slice(0, 6),
cntr: (document.getElementById('sp-cntr').value || '0000000001').replace(/[^0-9a-fA-F]/g, ''),
kicKey: (document.getElementById('sp-kic-key').value || '').replace(/[^0-9a-fA-F]/g, ''),
kidKey: (document.getElementById('sp-kid-key').value || '').replace(/[^0-9a-fA-F]/g, ''),
@@ -9762,8 +9765,11 @@ function cardsAutoSelectByIccid(iccid) {
}
// Fill the SP form from the preset (no packet generation): the form is a
-// working copy, the preset stays the source of truth.
-function cardsApplyFields(idx) {
+// working copy, the preset stays the source of truth. `keepTar` leaves the
+// packet TAR alone: the plain SCP80 send re-reads only the preset-owned
+// counter/keys/SPI, a manually entered target TAR (push/link triggers,
+// applet OTA) must survive.
+function cardsApplyFields(idx, keepTar) {
const c = cards[parseInt(idx)];
if (!c) return false;
document.getElementById('sp-spi1').value = c.spi1;
@@ -9791,7 +9797,7 @@ function cardsApplyFields(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 = cardsTarValue(_spTarKey, c[_spTarKey]);
+ if (!keepTar) 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;
@@ -9800,8 +9806,8 @@ function cardsApplyFields(idx) {
return true;
}
-function cardsApply(idx) {
- if (!cardsApplyFields(idx)) return;
+function cardsApply(idx, keepTar) {
+ if (!cardsApplyFields(idx, keepTar)) return;
genSp();
}
@@ -9810,12 +9816,12 @@ function cardsApply(idx) {
// filled, and a stale counter is rejected by the card (cntr_low) and would
// then be written back over the preset. Returns the preset counter ('' when
// no preset is selected).
-function spRefreshFromPreset(selId) {
+function spRefreshFromPreset(selId, keepTar) {
const sel = document.getElementById(selId || 'sp-card-sel');
const idx = sel ? parseInt(sel.value, 10) : NaN;
if (isNaN(idx) || !cards[idx]) return '';
if (selId === 'ram-card-sel') _spTarKey = 'tar';
- cardsApply(idx);
+ cardsApply(idx, keepTar);
return cards[idx].cntr;
}
diff --git a/frontend/sw.js b/frontend/sw.js
index ead91e0..4165a3e 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'simple-v293';
+const CACHE = 'simple-v294';
const URLS = [
'index.html',
'help.html',
diff --git a/frontend/tests/cards_counter.test.js b/frontend/tests/cards_counter.test.js
index 470c00a..584ff1e 100644
--- a/frontend/tests/cards_counter.test.js
+++ b/frontend/tests/cards_counter.test.js
@@ -29,7 +29,8 @@ for (const fn of ['cardsTarValue', 'cardsApplyFields', 'cardsApply',
eval(code);
// The SP form is a working copy: the preset is the source of truth, and
-// every SCP80/RAM operation re-reads it before starting (v3.6.3).
+// every SCP80/RAM operation re-reads it before starting (v3.6.3). The plain
+// SCP80 send keeps a hand-edited packet TAR (v3.6.21).
function fakeEnv(selValue, selId, presetCntr) {
const els = {};
for (const id of ['sp-spi1', 'sp-spi2-hex', 'sp-kic-idx', 'sp-kic-alg',
@@ -70,6 +71,17 @@ test('spRefreshFromPreset for the RAM selector targets the ISD TAR', () => {
assert.strictEqual(env.els['sp-tar'].value, '000000');
});
+test('the pre-send refresh keeps a hand-edited packet TAR (v3.6.21)', () => {
+ const env = fakeEnv('0', 'sp-card-sel', '0000000020');
+ env.els['sp-tar'].value = 'AF4D01'; // e.g. a push/link trigger target
+ assert.strictEqual(spRefreshFromPreset('sp-card-sel', true), '0000000020');
+ assert.strictEqual(env.els['sp-tar'].value, 'AF4D01', 'the typed TAR must survive the send');
+ assert.strictEqual(env.els['sp-cntr'].value, '0000000020', 'the counter still refreshes');
+ // an explicit preset apply (or a pack) still sets the TAR
+ cardsApplyFields(0);
+ assert.strictEqual(env.els['sp-tar'].value, 'B00000');
+});
+
test('cardsApplyFields fills the form without generating a packet', () => {
const env = fakeEnv('0', 'sp-card-sel', '0000000007');
env.els['sp-cntr'].value = 'nonsense';
diff --git a/frontend/tests/ram.test.js b/frontend/tests/ram.test.js
index 7778c1e..1fac6c0 100644
--- a/frontend/tests/ram.test.js
+++ b/frontend/tests/ram.test.js
@@ -499,6 +499,11 @@ test('getRamSpParams reads the computed SPI2 byte, not the base select', () => {
assert.ok(/spi2: document\.getElementById\('sp-spi2-hex'\)/.test(fn), fn);
});
+test('getRamSpParams normalises the packet TAR to three bytes', () => {
+ const fn = extractFunc(html, 'getRamSpParams');
+ assert.ok(/tar:[\s\S]*?padEnd\(6, '0'\)\.slice\(0, 6\),/.test(fn), fn);
+});
+
test('ramListingSpi2 requests the SMS-submit PoR for the listing queries', () => {
// Apps (40) and ELF (20/10) listings can exceed the ENVELOPE response;
// with SPI2=0x01 the card answers actual_response_sms_submit and the data
diff --git a/pyproject.toml b/pyproject.toml
index 2224f0e..02a47e0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
-version = "3.6.20"
+version = "3.6.21"
description = "HTTP REST server wrapping pysim for the SIMple PWA"
requires-python = ">=3.8"
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py
index 4388753..0ccc3b5 100644
--- a/pysim_simple_server/server.py
+++ b/pysim_simple_server/server.py
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
-VERSION = '3.6.20'
+VERSION = '3.6.21'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE