Compare commits

...

7 Commits

Author SHA1 Message Date
catarrh 1ce23f021c C-APDU Parser: change Parse button to Decode (existing i18n key) 2026-08-20 22:21:39 +03:00
catarrh 72b0116bad C-APDU Parser: client-side hex parser for APDU/expanded script decode
New 'Parser' subtab in C-APDU section. Detects format (compact C-APDU
chain, expanded AA/AE80 script) and renders collapsible tree with decoded
parameters. Key features:
- BER-TLV parser with multi-byte tag support and CR-bit handling
- Proactive command decoder (types from BER_QUAL, devices from BER_DEVICES)
- SIM/UICC toolkit parameter decoder (CA/EA/80)
- GSM7 text decode (7-bit packed, escape sequences)
- Best-effort UCS2/GSM7 text detection
- Compact C-APDU chain with implied-CLA support (SELECT + op chaining)
- 18 test cases from exchange vectors (apdu_parse.test.js)
2026-08-20 22:11:55 +03:00
catarrh 8019326cfb Error Action: render builder directly, remove redundant subtype select
Error Action (tag 82) now renders buildErrorActionRow() directly in
onBerTypeChange, bypassing the shared ber-subtype select which had
confusing 'Action indicator' option and dead 'Proactive command'/'Custom
hex' choices. Removed the now-unreachable type === 'error' branch from
onBerSubTypeChange.
2026-08-20 21:39:59 +03:00
catarrh 01931b9693 Fix Error Action proactive: filter REFRESH out of allowed commands
When Error Action (tag 82) uses the Proactive command subtype, only
DISPLAY TEXT and PLAY TONE are allowed per TS 102 226 Table 5.9.
Pass allowed=['display','tone'] to buildBerPcHtml for error-type rows.
2026-08-20 21:34:38 +03:00
catarrh 03ec13364c Fix onErrorActionTypeChange: sel.nextElementSibling -> sel.parentElement.nextElementSibling
The Error Action HTML structure wraps the select in a flex div sibling to
the .error-action-body div, so sel.nextElementSibling was null.
2026-08-20 21:29:07 +03:00
catarrh e6602c40bf RAM constructor fixes: EA/CA nesting, access domain order, menu pairs, BER lengths, services byte (TS 102 226)
- UICC Toolkit tag 80 nested inside EA (EA 12 80 10 ...)
- ram-tk-mode value ea (was 80), label 'UICC Toolkit (Tag EA)'
- SIM (CA) access domain FIRST in payload, blank = 00
- Menu entries: 2×m pairs (i=1 first, i=m last, else 0000)
- CA/EA lengths via berLenStr() (supports 81xx long form)
- UICC trailing 'Maximum number of services' byte (ram-tk-services)
- DELETE: P1=00 fixed, mode in P2 (80E4 00 <mode> ...)
- STORE DATA: ram-enc 00/40/80/C0/E0 with correct labels
- LOAD: P1 fixed to 0x80 (last block)
- USIM SELECT: selP1 0x09 -> 0x00 (select by file id)
- ram.test.js: 11 exact-hex tests from exchange vectors
- v1.8.1, SW cache otaman-v9
2026-08-20 21:23:37 +03:00
catarrh 4a6a142c3d Expanded Format spec fixes: REFRESH qualifiers, Error Action forms, Script Chaining flags, CR-bit tags (TS 102 226)
- BER_QUAL.refresh: full TS 102 223 §8.6 table (00-0A)
- Error Action rebuilt as single select: Proactive (DISPLAY/PLAY TONE only) /
  No action (82 00) / EFRMA ref (82 01 xx) / Custom hex; drop bogus
  Action Type / Trigger Conditions / Retry Count UI
- buildBerPcHtml(allowed): no Custom option; typeBytes tone 31 -> 20
- DISPLAY TEXT Duration control emitting 84 02 <unit> <interval>
- File list tag 12 -> 92 (CR bit set)
- Script Chaining flags 01/11/02/03 + keep-across-reset checkbox (RFM)
- ber.test.js: exact-hex tests for all above (exchange vectors)
2026-08-20 21:13:41 +03:00
10 changed files with 1407 additions and 256 deletions
+1 -1
View File
@@ -54,7 +54,7 @@ Returns server version for compatibility checking.
**Example response:**
```json
{"version": "1.8.0"}
{"version": "1.8.1"}
```
### `GET /api/status`
+6 -6
View File
@@ -110,18 +110,18 @@
<p class="text-sm mb-3">Сборщик Immediate Action предлагает action indicator (<code class="font-mono text-sm">81</code>/<code class="font-mono text-sm">82</code>), структурированный сборщик проактивных команд (REFRESH, DISPLAY TEXT, PLAY TONE с авто-генерацией COMPREHENSION-TLV), или ручной hex-ввод.</p>
<h4 id="ber-error-action" class="font-medium mb-2 text-base">Error Action TLV (Tag 82)</h4>
<p class="text-sm mb-2">Условное восстановление при ошибках для graceful rollback:</p>
<p class="text-sm mb-2">Восстановление при ошибках по TS 102 226 §5.2.1.3 — одна из трёх форм:</p>
<ul class="text-sm list-disc pl-5 mb-2">
<li><strong>Тип действия:</strong> Немедленное (00), отложенное (01) или условное (02) выполнение</li>
<li><strong>Условия запуска:</strong> Сопоставление SW (любая ошибка, конкретный SW, диапазон SW, конкретная команда)</li>
<li><strong>Действие восстановления:</strong> Action Indicator (81 session indication / 82 early response) или проактивная команда (DISPLAY TEXT, PLAY TONE, REFRESH) для уведомления или отката</li>
<li><strong>Количество попыток:</strong> Число повторных попыток до отказа (0=без повтора)</li>
<li><strong>Проактивная команда:</strong> набор COMPREHENSION-TLV с DISPLAY TEXT или PLAY TONE (в Error Action допустимы только эти две, TS 102 226 Table 5.9)</li>
<li><strong>Без действия:</strong> <code class="font-mono text-sm">82 00</code></li>
<li><strong>Ссылка на запись EFRMA:</strong> <code class="font-mono text-sm">82 01 &lt;ref&gt;</code> с номером записи <code class="font-mono text-sm">01</code><code class="font-mono text-sm">7F</code></li>
<li><strong>Произвольный hex:</strong> произвольное значение TLV</li>
</ul>
<h4 id="ber-script-chaining" class="font-medium mb-2 text-base">Script Chaining TLV (Tag 83)</h4>
<p class="text-sm mb-2">Многопакетное выполнение скрипта с сохранением контекста:</p>
<ul class="text-sm list-disc pl-5 mb-2">
<li><strong>Флаги цепочки:</strong> Позиция First/Intermediary/Last Script (бит 0/1/2)</li>
<li><strong>Флаги цепочки:</strong> <code class="font-mono text-sm">01</code> первый скрипт (удалять инфо о цепочке при сбросе), <code class="font-mono text-sm">11</code> первый скрипт (сохранять инфо о цепочке при сбросе, только RFM), <code class="font-mono text-sm">02</code> последующий скрипт (будут ещё), <code class="font-mono text-sm">03</code> последующий скрипт (последний)</li>
<li><strong>Идентификатор скрипта:</strong> Корреляционный идентификатор между пакетами (1-4 байта hex, авто-инкремент подсказки)</li>
<li><strong>Дополнительные данные:</strong> Расширенная информация цепочки (опционально hex)</li>
<li><strong>Сохранение контекста:</strong> UICC сохраняет состояние безопасности/транзакции между пакетами</li>
+6 -6
View File
@@ -110,18 +110,18 @@
<p class="text-sm mb-3">The Immediate Action builder offers an action indicator (<code class="font-mono text-sm">81</code>/<code class="font-mono text-sm">82</code>), a structured proactive command builder (REFRESH, DISPLAY TEXT, PLAY TONE with auto-generated COMPREHENSION-TLV objects), or a freeform hex input.</p>
<h4 id="ber-error-action" class="font-medium mb-2 text-base">Error Action TLV (Tag 82)</h4>
<p class="text-sm mb-2">Conditional error recovery for graceful rollback:</p>
<p class="text-sm mb-2">Error recovery per TS 102 226 §5.2.1.3 — one of three forms:</p>
<ul class="text-sm list-disc pl-5 mb-2">
<li><strong>Action Type:</strong> Immediate (00), delayed (01), or conditional (02) execution</li>
<li><strong>Trigger Conditions:</strong> SW pattern matching (any error, specific SW, SW range, specific command fails)</li>
<li><strong>Recovery Action:</strong> Action Indicator (81 session indication / 82 early response) or Proactive Command (DISPLAY TEXT, PLAY TONE, REFRESH) to notify user or fallback</li>
<li><strong>Retry Count:</strong> Number of retry attempts before giving up (0=no retry)</li>
<li><strong>Proactive command:</strong> COMPREHENSION-TLV set with DISPLAY TEXT or PLAY TONE (only these two are allowed in an Error Action, TS 102 226 Table 5.9)</li>
<li><strong>No action:</strong> <code class="font-mono text-sm">82 00</code></li>
<li><strong>Reference to EFRMA record:</strong> <code class="font-mono text-sm">82 01 &lt;ref&gt;</code> with record number <code class="font-mono text-sm">01</code><code class="font-mono text-sm">7F</code></li>
<li><strong>Custom hex:</strong> arbitrary TLV value</li>
</ul>
<h4 id="ber-script-chaining" class="font-medium mb-2 text-base">Script Chaining TLV (Tag 83)</h4>
<p class="text-sm mb-2">Multi-packet script execution with context preservation:</p>
<ul class="text-sm list-disc pl-5 mb-2">
<li><strong>Chaining Flags:</strong> First/Intermediary/Last Script position (bit 0/1/2)</li>
<li><strong>Chaining Flags:</strong> <code class="font-mono text-sm">01</code> first script (delete chaining info on reset), <code class="font-mono text-sm">11</code> first script (keep chaining info across reset, RFM only), <code class="font-mono text-sm">02</code> subsequent script (more to follow), <code class="font-mono text-sm">03</code> subsequent script (last)</li>
<li><strong>Script ID:</strong> Correlation identifier across packets (1-4 bytes hex, auto-increment hints provided)</li>
<li><strong>Additional Data:</strong> Extended chaining information (optional hex)</li>
<li><strong>Context Preservation:</strong> UICC keeps security/transaction state open across chained scripts</li>
+642 -240
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'otaman-v8';
const CACHE = 'otaman-v9';
const URLS = [
'index.html',
'help.html',
+176
View File
@@ -0,0 +1,176 @@
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);
}
function findNode(tree, label) {
if (tree.label === label) return tree;
if (tree.children) {
for (const c of tree.children) {
const r = findNode(c, label);
if (r) return r;
}
}
return null;
}
function findNodes(tree, label) {
const out = [];
if (tree.label === label) out.push(tree);
if (tree.children) {
for (const c of tree.children) out.push(...findNodes(c, label));
}
return out;
}
const consts = ['BER_QUAL', 'BER_DEVICES', 'BER_TONES', 'GSM7_ALPHABET', 'GSM7_EXT_MAP'];
let prefix = '';
for (const c of consts) {
const m = html.match(new RegExp('const\\s+' + c + '\\s*=\\s*(?:\\[|\\{)[\\s\\S]*?(?:\\n[\\]\\}];|[\\]\\}];\\n)'));
if (!m) throw new Error(c + ' not found');
prefix += m[0].replace(/^const /, 'var ') + '\n';
}
const start = html.indexOf('// ===== C-APDU Parser =====');
const end = html.indexOf('// ===== Response Parser =====');
eval(prefix + html.slice(start, end).replace(/^const PARSE_INS = /m, 'var PARSE_INS = '));
test('Compact RAM INSTALL [for install] UICC', () => {
const tree = parseHexTree('80E60C00214F08A000000151000000C70100EA128010000000020101020200011603B0000100');
assert.strictEqual(tree.label, 'Compact C-APDU chain');
assert.strictEqual(tree.children.length, 1);
const apdu = tree.children[0];
assert.strictEqual(apdu.label, 'APDU');
assert.ok(findNode(apdu, 'INS').desc.includes('INSTALL'));
});
test('Compact RAM INSTALL [for install] SIM (CA) access domain 5A', () => {
const tree = parseHexTree('80E60C00204F08A000000151000000C70100CA11015A000000020101020200011603B00001');
const apdu = tree.children[0];
assert.ok(findNode(apdu, 'INS').desc.includes('INSTALL'));
});
test('Expanded AA DISPLAY TEXT UCS2 "HI" + 5s', () => {
const tree = parseHexTree('AA1681130103012101020281820D040048004904020105');
assert.strictEqual(tree.label, 'Expanded Script (AA)');
assert.strictEqual(tree.children.length, 1);
assert.strictEqual(tree.children[0].label, 'Immediate Action');
});
test('Expanded AA DISPLAY TEXT GSM7 "HI"', () => {
const tree = parseHexTree('AA0F810D0103012101020281820D02C824');
assert.strictEqual(tree.children.length, 1);
assert.strictEqual(tree.children[0].label, 'Immediate Action');
});
test('Expanded AA REFRESH + file list', () => {
const tree = parseHexTree('AA10810E01030101000202818212036F3B2F');
assert.strictEqual(tree.children.length, 1);
assert.strictEqual(tree.children[0].label, 'Immediate Action');
});
test('Expanded AE80 PLAY TONE + Error Action no action', () => {
const tree = parseHexTree('AE80810C0103200101020281820E010182000000');
assert.strictEqual(tree.label, 'Expanded Script (AE80)');
assert.strictEqual(tree.children.length, 2);
assert.strictEqual(tree.children[0].label, 'Immediate Action');
assert.strictEqual(tree.children[1].label, 'Error Action');
});
test('Immediate Action CR-set tags (real-world encoding)', () => {
const tree = parseHexTree('AA0E8101818109810301010482028182');
assert.strictEqual(tree.children.length, 2);
assert.strictEqual(tree.children[0].label, 'Immediate Action');
assert.strictEqual(tree.children[1].label, 'Immediate Action');
});
test('CR-set DISPLAY TEXT UCS2 "HI" + 5s', () => {
const tree = parseHexTree('AA1581138103012101820281828D040048004984020105');
assert.strictEqual(tree.children.length, 1);
});
test('CR-set PLAY TONE (tag 8E)', () => {
const tree = parseHexTree('AA0E810C8103012001820281828E0101');
assert.strictEqual(tree.children.length, 1);
});
test('CR-set REFRESH file list (tag 92)', () => {
const tree = parseHexTree('AA0F810D81030101048202818292023F00');
assert.strictEqual(tree.children.length, 1);
});
test('Expanded AA Error Action = proactive DISPLAY TEXT', () => {
const tree = parseHexTree('AA0F820D0103012101020281820D022852');
assert.strictEqual(tree.children.length, 1);
assert.strictEqual(tree.children[0].label, 'Error Action');
});
test('Expanded AA 22 C-APDU row (GET STATUS)', () => {
const tree = parseHexTree('AA09220780F22000024F00');
assert.strictEqual(tree.children.length, 1);
assert.strictEqual(tree.children[0].label, 'C-APDU');
});
test('Chained SIM SELECT + UPDATE RECORD', () => {
const tree = parseHexTree('A0A40000026F3BDC0102032B2F2D');
assert.strictEqual(tree.label, 'Compact C-APDU chain');
assert.strictEqual(tree.children.length, 2);
assert.strictEqual(tree.children[0].label, 'APDU');
});
test('INTERNAL AUTHENTICATE', () => {
const tree = parseHexTree('8088000008800F3495BA2355CD');
assert.strictEqual(tree.children.length, 1);
const apdu = tree.children[0];
assert.strictEqual(apdu.label, 'APDU');
assert.ok(findNode(apdu, 'INS').desc.includes('INTERNAL AUTHENTICATE'));
assert.ok(findNode(apdu, 'Data'));
});
test('baseCompTag clears CR bit for 0x80-0x9F', () => {
assert.strictEqual(baseCompTag('81'), '01');
assert.strictEqual(baseCompTag('82'), '02');
assert.strictEqual(baseCompTag('8D'), '0D');
assert.strictEqual(baseCompTag('8E'), '0E');
assert.strictEqual(baseCompTag('92'), '12');
assert.strictEqual(baseCompTag('84'), '04');
assert.strictEqual(baseCompTag('85'), '05');
assert.strictEqual(baseCompTag('01'), '01');
assert.strictEqual(baseCompTag('22'), '22');
});
test('parseTlvList handles BER-TLVs', () => {
const tlvs = parseTlvList('810301210182028182');
assert.strictEqual(tlvs.length, 2);
assert.strictEqual(tlvs[0].tag, '81');
assert.strictEqual(tlvs[0].length, 3);
assert.strictEqual(tlvs[0].value, '012101');
assert.strictEqual(tlvs[1].tag, '82');
});
test('gsm7Decode unpacks "HI" from C824', () => {
const bytes = new Uint8Array([0xC8, 0x24]);
assert.strictEqual(gsm7Decode(bytes), 'HI');
});
test('decodePrivileges', () => {
assert.ok(decodePrivileges('00').includes('None'));
assert.ok(decodePrivileges('80').includes('Security Domain'));
});
+280
View File
@@ -0,0 +1,280 @@
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);
}
class StubEl {
constructor(value = '') {
this.value = value;
this.checked = false;
this.disabled = false;
this.children = [];
this.parentElement = null;
this.nextElementSibling = null;
}
querySelector(sel) {
return this._find(sel);
}
querySelectorAll(sel) {
const out = [];
this._findAll(sel, out);
return out;
}
_find(sel) {
for (const c of this.children) {
if (c._matches(sel)) return c;
const r = c._find(sel);
if (r) return r;
}
return null;
}
_findAll(sel, out) {
for (const c of this.children) {
if (c._matches(sel)) out.push(c);
c._findAll(sel, out);
}
}
_matches(sel) {
if (sel.startsWith('.')) return this.cls === sel.slice(1);
if (sel.startsWith('input[name="')) {
const m = sel.match(/^input\[name="([^"]+)"\]\[value="([^"]+)"\]$/);
if (m) return this.name === m[1] && this.value === m[2];
const m2 = sel.match(/^input\[name="([^"]+)"\]$/);
if (m2) return this.name === m2[1];
}
return false;
}
}
function el(cls, value = '') {
const e = new StubEl(value);
e.cls = cls;
return e;
}
function buildRow() {
const row = new StubEl();
row.cls = 'ber-row';
row.dataset = { berIdx: '0' };
const body = el('error-action-body');
const type = el('error-action-type', 'proactive');
type.nextElementSibling = body;
body.parentElement = row;
row.children.push(type, body);
return row;
}
function buildPcHtml(extraFields) {
// Build the .ber-pc tree expected by genBerPcValue
const pc = el('ber-pc');
const type = el('ber-pc-type', 'display');
const num = el('ber-pc-num', '1');
const qual = el('ber-pc-qual', '01');
const src = el('ber-pc-src', '81');
const dst = el('ber-pc-dst', '82');
const text = el('ber-pc-text', 'HI');
const enc = el('ber-pc-enc', 'ucs2');
const alpha = el('ber-pc-alpha', '');
const durEnable = el('ber-pc-dur-enable');
durEnable.checked = true;
const durUnit = el('ber-pc-dur-unit', '01');
const durVal = el('ber-pc-dur-val', '30');
pc.children.push(type, num, qual, src, dst, text, enc, alpha, durEnable, durUnit, durVal);
if (extraFields) extraFields(pc);
return pc;
}
const FNS = ['berLenStr', 'gsm7TextToSeptets', 'gsm7Encode', 'genBerPcValue', 'genErrorActionValue',
'genScriptChainingValue'];
let code = '';
for (const f of FNS) code += extractFunc(html, f) + '\n';
const berTagsMatch = html.match(/const BER_TAGS = \{[^}]*\};/);
const berQualMatch = html.match(/const BER_QUAL = \{[\s\S]*?\n\};/);
const berDevMatch = html.match(/const BER_DEVICES = \[[\s\S]*?\];/);
const berTonesMatch = html.match(/const BER_TONES = \[[\s\S]*?\];/);
const gsm7AlphaMatch = html.match(/const GSM7_ALPHABET = \[[\s\S]*?\n\];/);
const gsm7ExtMatch = html.match(/const GSM7_EXT_MAP = \{[\s\S]*?\n\};/);
for (const m of [berTagsMatch, berQualMatch, berDevMatch, berTonesMatch, gsm7AlphaMatch, gsm7ExtMatch]) {
if (!m) throw new Error('constant not found');
code = m[0] + '\n' + code;
}
code += '\nvar __berConsts = {BER_TAGS, BER_QUAL, BER_DEVICES, BER_TONES, GSM7_ALPHABET, GSM7_EXT_MAP};';
eval(code);
const {BER_TAGS, BER_QUAL, BER_DEVICES, BER_TONES, GSM7_ALPHABET, GSM7_EXT_MAP} = __berConsts;
function mkRow(type) {
const row = new StubEl();
row.cls = 'ber-row';
row.dataset = { berIdx: '0' };
const body = el('error-action-body');
const actionType = el('error-action-type', type);
actionType.nextElementSibling = body;
row.children.push(actionType, body);
return row;
}
test('BER_QUAL.refresh matches TS 102 223 §8.6 mapping', () => {
const r = BER_QUAL.refresh;
assert.strictEqual(r[0][0], '00');
assert.strictEqual(r[0][1], 'NAA Initialization and Full File Change Notification');
assert.strictEqual(r[4][0], '04');
assert.strictEqual(r[4][1], 'UICC Reset');
assert.strictEqual(r[5][1], 'NAA Application Reset (not for 2G SIM)');
assert.strictEqual(r[6][1], 'NAA Session Reset (not for 2G SIM)');
assert.strictEqual(r[7][1], 'Steering of Roaming');
assert.strictEqual(r.length, 11);
});
test('genBerPcValue DISPLAY TEXT GSM7 "HI"', () => {
const pc = buildPcHtml();
pc.children.find(c => c.cls === 'ber-pc-enc').value = 'gsm7';
pc.children.find(c => c.cls === 'ber-pc-dur-enable').checked = false;
const val = genBerPcValue({ querySelector: (s) => s === '.ber-pc' ? pc : null }, '81');
assert.strictEqual(val, '810D8103012101820281828D02C824');
});
test('genBerPcValue DISPLAY TEXT UCS2 "HI" + 30s duration', () => {
const pc = buildPcHtml();
const val = genBerPcValue({ querySelector: (s) => s === '.ber-pc' ? pc : null }, '81');
// 81 13 8103 01 21 01 8202 81 82 8D 04 0048 0049 84 02 01 1E
assert.strictEqual(val, '81138103012101820281828D04004800498402011E');
});
test('genBerPcValue duration disabled omits 84', () => {
const pc = buildPcHtml();
pc.children.find(c => c.cls === 'ber-pc-dur-enable').checked = false;
const val = genBerPcValue({ querySelector: (s) => s === '.ber-pc' ? pc : null }, '81');
assert.ok(!val.includes('84'));
});
test('genBerPcValue PLAY TONE uses type byte 20', () => {
const pc = buildPcHtml();
pc.children.find(c => c.cls === 'ber-pc-type').value = 'tone';
pc.children.find(c => c.cls === 'ber-pc-qual').value = '00';
pc.children.push(el('ber-pc-tone', '01'));
const val = genBerPcValue({ querySelector: (s) => s === '.ber-pc' ? pc : null }, '81');
assert.strictEqual(val, '810C8103012000820281828E0101');
});
test('genBerPcValue REFRESH uses CR-set file list tag 92', () => {
const pc = buildPcHtml();
pc.children.find(c => c.cls === 'ber-pc-type').value = 'refresh';
pc.children.find(c => c.cls === 'ber-pc-qual').value = '04';
pc.children.push(el('ber-pc-flist', '3F00'));
const val = genBerPcValue({ querySelector: (s) => s === '.ber-pc' ? pc : null }, '81');
assert.strictEqual(val, '810D81030101048202818292023F00');
});
test('genErrorActionValue noaction emits 82 00', () => {
const row = mkRow('noaction');
const val = genErrorActionValue(row, '82');
assert.strictEqual(val, '8200');
});
test('genErrorActionValue reference emits 82 01 ref', () => {
const row = mkRow('reference');
row.children[1].children.push(el('error-ref', '05'));
const val = genErrorActionValue(row, '82');
assert.strictEqual(val, '820105');
});
test('genErrorActionValue reference rejects out-of-range ref', () => {
const row = mkRow('reference');
row.children[1].children.push(el('error-ref', '80'));
assert.strictEqual(genErrorActionValue(row, '82'), '');
});
test('genErrorActionValue proactive wraps proactive command', () => {
const row = mkRow('proactive');
const pc = buildPcHtml();
row.children[1].children.push(pc);
const val = genErrorActionValue(row, '82');
// DISPLAY TEXT UCS2 "HI" + 30s inside Error Action
assert.strictEqual(val, '82138103012101820281828D04004800498402011E');
});
test('genScriptChainingValue first emits 01', () => {
const row = new StubEl();
row.dataset = { berIdx: '0' };
const first = el('chaining-first', 'first');
first.name = 'chaining-0';
first.checked = true;
const interm = el('chaining-inter', 'intermediary');
interm.name = 'chaining-0';
const last = el('chaining-last', 'last');
last.name = 'chaining-0-last';
const keep = el('chaining-keep', 'keep');
keep.name = 'chaining-0-keep';
row.children.push(first, interm, last, keep, el('chaining-script-id'), el('chaining-additional'));
const val = genScriptChainingValue(row, '83');
assert.strictEqual(val, '830101');
});
test('genScriptChainingValue first with keep emits 11', () => {
const row = new StubEl();
row.dataset = { berIdx: '0' };
const first = el('chaining-first', 'first');
first.name = 'chaining-0';
first.checked = true;
const interm = el('chaining-inter', 'intermediary');
interm.name = 'chaining-0';
const last = el('chaining-last', 'last');
last.name = 'chaining-0-last';
const keep = el('chaining-keep', 'keep');
keep.name = 'chaining-0-keep';
keep.checked = true;
row.children.push(first, interm, last, keep, el('chaining-script-id'), el('chaining-additional'));
const val = genScriptChainingValue(row, '83');
assert.strictEqual(val, '830111');
});
test('genScriptChainingValue intermediary last emits 03', () => {
const row = new StubEl();
row.dataset = { berIdx: '0' };
const first = el('chaining-first', 'first');
first.name = 'chaining-0';
const interm = el('chaining-inter', 'intermediary');
interm.name = 'chaining-0';
interm.checked = true;
const last = el('chaining-last', 'last');
last.name = 'chaining-0-last';
last.checked = true;
const keep = el('chaining-keep', 'keep');
keep.name = 'chaining-0-keep';
row.children.push(first, interm, last, keep, el('chaining-script-id'), el('chaining-additional'));
const val = genScriptChainingValue(row, '83');
assert.strictEqual(val, '830103');
});
test('genScriptChainingValue no position emits empty', () => {
const row = new StubEl();
row.dataset = { berIdx: '0' };
const first = el('chaining-first', 'first');
first.name = 'chaining-0';
const interm = el('chaining-inter', 'intermediary');
interm.name = 'chaining-0';
const last = el('chaining-last', 'last');
last.name = 'chaining-0-last';
const keep = el('chaining-keep', 'keep');
keep.name = 'chaining-0-keep';
row.children.push(first, interm, last, keep, el('chaining-script-id'), el('chaining-additional'));
assert.strictEqual(genScriptChainingValue(row, '83'), '');
});
+293
View File
@@ -0,0 +1,293 @@
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);
}
class StubEl {
constructor(value = '') {
this.value = value;
this.checked = false;
this.style = {};
}
}
const els = {};
function el(id, value = '') {
if (!els[id]) els[id] = new StubEl(value);
return els[id];
}
function reset() {
for (const id of Object.keys(els)) delete els[id];
}
const FNS = ['berLenStr', 'buildApdu', 'genRam'];
let code = '';
for (const f of FNS) code += extractFunc(html, f) + '\n';
eval(code);
// document.getElementById stub: global getElementById in the extracted functions
const doc = {
getElementById: (id) => {
if (!els[id]) els[id] = new StubEl();
return els[id];
},
};
// genRam also references document.getElementById via global document
global.document = doc;
function set(id, v) { el(id).value = v; }
function setChecked(id, v) { el(id).checked = v; }
function genRamResult() {
genRam();
return els['ram-result'].value;
}
function genRamApdu() {
reset();
set('ram-cmd', 'install-install');
set('ram-aid', 'A000000151000000');
set('ram-priv', '00');
setChecked('ram-toolkit-enable', true);
set('ram-tk-mode', 'ea');
set('ram-tk-priority', '0');
set('ram-tk-timers', '0');
set('ram-tk-textlen', '0');
set('ram-tk-menus', '2');
set('ram-tk-firstpos', '1');
set('ram-tk-firstid', '01');
set('ram-tk-lastpos', '2');
set('ram-tk-lastid', '02');
set('ram-tk-channels', '0');
set('ram-tk-msl', '16');
set('ram-tk-tar', 'B00001');
set('ram-tk-ad', '');
set('ram-tk-services', '0');
return genRamResult();
}
test('UICC toolkit nested inside EA (m=2, services 0)', () => {
const apdu = genRamApdu();
assert.ok(apdu.includes('EA128010000000020101020200011603B0000100'), apdu);
});
test('UICC m=1 emits single pair', () => {
reset();
set('ram-cmd', 'install-install');
set('ram-aid', 'A000000151000000');
set('ram-priv', '00');
setChecked('ram-toolkit-enable', true);
set('ram-tk-mode', 'ea');
set('ram-tk-priority', '0');
set('ram-tk-timers', '0');
set('ram-tk-textlen', '0');
set('ram-tk-menus', '1');
set('ram-tk-firstpos', '1');
set('ram-tk-firstid', '01');
set('ram-tk-lastpos', '0');
set('ram-tk-lastid', '00');
set('ram-tk-channels', '0');
set('ram-tk-msl', '16');
set('ram-tk-tar', 'B00001');
set('ram-tk-ad', '');
set('ram-tk-services', '0');
const apdu = genRamResult();
assert.ok(apdu.includes('EA10800E00000001010100011603B0000100'), apdu);
});
test('UICC m=3 fills middle pair with 0000', () => {
reset();
set('ram-cmd', 'install-install');
set('ram-aid', 'A000000151000000');
set('ram-priv', '00');
setChecked('ram-toolkit-enable', true);
set('ram-tk-mode', 'ea');
set('ram-tk-priority', '0');
set('ram-tk-timers', '0');
set('ram-tk-textlen', '0');
set('ram-tk-menus', '3');
set('ram-tk-firstpos', '1');
set('ram-tk-firstid', '01');
set('ram-tk-lastpos', '3');
set('ram-tk-lastid', '03');
set('ram-tk-channels', '0');
set('ram-tk-msl', '16');
set('ram-tk-tar', 'B00001');
set('ram-tk-ad', '');
set('ram-tk-services', '0');
const apdu = genRamResult();
assert.ok(apdu.includes('EA1480120000000301010000030300011603B0000100'), apdu);
});
test('UICC services 7 appended as final byte', () => {
reset();
set('ram-cmd', 'install-install');
set('ram-aid', 'A000000151000000');
set('ram-priv', '00');
setChecked('ram-toolkit-enable', true);
set('ram-tk-mode', 'ea');
set('ram-tk-priority', '0');
set('ram-tk-timers', '0');
set('ram-tk-textlen', '0');
set('ram-tk-menus', '2');
set('ram-tk-firstpos', '1');
set('ram-tk-firstid', '01');
set('ram-tk-lastpos', '2');
set('ram-tk-lastid', '02');
set('ram-tk-channels', '0');
set('ram-tk-msl', '16');
set('ram-tk-tar', 'B00001');
set('ram-tk-ad', '');
set('ram-tk-services', '7');
const apdu = genRamResult();
assert.ok(apdu.includes('EA128010000000020101020200011603B0000107'), apdu);
});
test('SIM (CA) access domain FIRST, no services byte', () => {
reset();
set('ram-cmd', 'install-install');
set('ram-aid', 'A000000151000000');
set('ram-priv', '00');
setChecked('ram-toolkit-enable', true);
set('ram-tk-mode', 'ca');
set('ram-tk-priority', '0');
set('ram-tk-timers', '0');
set('ram-tk-textlen', '0');
set('ram-tk-menus', '2');
set('ram-tk-firstpos', '1');
set('ram-tk-firstid', '01');
set('ram-tk-lastpos', '2');
set('ram-tk-lastid', '02');
set('ram-tk-channels', '0');
set('ram-tk-msl', '16');
set('ram-tk-tar', 'B00001');
set('ram-tk-ad', '5A');
set('ram-tk-services', '0');
const apdu = genRamResult();
assert.ok(apdu.includes('CA11015A000000020101020200011603B00001'), apdu);
});
test('SIM (CA) blank access domain emits length byte 00', () => {
reset();
set('ram-cmd', 'install-install');
set('ram-aid', 'A000000151000000');
set('ram-priv', '00');
setChecked('ram-toolkit-enable', true);
set('ram-tk-mode', 'ca');
set('ram-tk-priority', '0');
set('ram-tk-timers', '0');
set('ram-tk-textlen', '0');
set('ram-tk-menus', '2');
set('ram-tk-firstpos', '1');
set('ram-tk-firstid', '01');
set('ram-tk-lastpos', '2');
set('ram-tk-lastid', '02');
set('ram-tk-channels', '0');
set('ram-tk-msl', '16');
set('ram-tk-tar', 'B00001');
set('ram-tk-ad', '');
set('ram-tk-services', '0');
const apdu = genRamResult();
assert.ok(apdu.includes('CA1000000000020101020200011603B00001'), apdu);
});
test('SIM (CA) m=3, no TAR, blank access domain', () => {
reset();
set('ram-cmd', 'install-install');
set('ram-aid', 'A000000151000000');
set('ram-priv', '00');
setChecked('ram-toolkit-enable', true);
set('ram-tk-mode', 'ca');
set('ram-tk-priority', '0');
set('ram-tk-timers', '0');
set('ram-tk-textlen', '0');
set('ram-tk-menus', '3');
set('ram-tk-firstpos', '1');
set('ram-tk-firstid', '01');
set('ram-tk-lastpos', '3');
set('ram-tk-lastid', '03');
set('ram-tk-channels', '0');
set('ram-tk-msl', '16');
set('ram-tk-tar', '');
set('ram-tk-ad', '');
set('ram-tk-services', '0');
const apdu = genRamResult();
assert.ok(apdu.includes('CA0F000000000301010000030300011600'), apdu);
});
test('UICC m=60 uses long-form BER lengths (EA 81 87 / inner 81 84)', () => {
reset();
set('ram-cmd', 'install-install');
set('ram-aid', 'A000000151000000');
set('ram-priv', '00');
setChecked('ram-toolkit-enable', true);
set('ram-tk-mode', 'ea');
set('ram-tk-priority', '0');
set('ram-tk-timers', '0');
set('ram-tk-textlen', '0');
set('ram-tk-menus', '60');
set('ram-tk-firstpos', '1');
set('ram-tk-firstid', '01');
set('ram-tk-lastpos', '60');
set('ram-tk-lastid', '3C');
set('ram-tk-channels', '0');
set('ram-tk-msl', '16');
set('ram-tk-tar', 'B00001');
set('ram-tk-ad', '');
set('ram-tk-services', '0');
const apdu = genRamResult();
assert.ok(apdu.includes('EA8187808184'), apdu);
});
test('LOAD P1 fixed to 80 (last block)', () => {
reset();
set('ram-cmd', 'load');
set('ram-data', 'AABBCC');
set('ram-block', '0');
const apdu = genRamResult();
assert.ok(apdu.startsWith('80E88000'), apdu);
});
test('DELETE: P1=00, mode in P2', () => {
reset();
set('ram-cmd', 'delete');
set('ram-aid', 'AA1902BC225501');
set('ram-del-mode', '00');
let apdu = genRamResult();
assert.ok(apdu.startsWith('80E40000'), apdu);
set('ram-del-mode', '80');
apdu = genRamResult();
assert.ok(apdu.startsWith('80E40080'), apdu);
});
test('STORE DATA ram-enc P1 values 00/40/80/C0/E0', () => {
for (const [enc, p1] of [['00','00'],['40','40'],['80','80'],['C0','C0'],['E0','E0']]) {
reset();
set('ram-cmd', 'store-data');
set('ram-data', 'AABB');
set('ram-enc', enc);
set('ram-block', '0');
const apdu = genRamResult();
assert.ok(apdu.startsWith('80E2' + p1 + '00'), enc + ' -> P1 ' + p1);
}
});
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-otaman-server"
version = "1.8.0"
version = "1.8.1"
description = "HTTP REST server wrapping pysim for the OTAMan PWA"
requires-python = ">=3.8"
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
+1 -1
View File
@@ -18,7 +18,7 @@ from osmocom.construct import GsmOrUcs2Adapter
from osmocom.tlv import BER_TLV_IE
VERSION = '1.8.0'
VERSION = '1.8.1'
# Static file serving (the PWA lives in <repo>/frontend, served by this server