diff --git a/docs/api.md b/docs/api.md
index b4f522a..6eb788f 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -54,7 +54,7 @@ Returns server version for compatibility checking.
**Example response:**
```json
-{"version": "1.8.0"}
+{"version": "1.8.1"}
```
### `GET /api/status`
diff --git a/frontend/index.html b/frontend/index.html
index 24e94d5..96af86e 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -17,7 +17,7 @@
-
OTAMan SIM OTA with a Human Face v1.8.0
+
OTAMan SIM OTA with a Human Face v1.8.1
github
@@ -379,7 +379,7 @@
@@ -438,6 +438,10 @@
+
+
+
+
@@ -456,10 +460,11 @@
@@ -1082,7 +1087,7 @@ function updateSimSelectSection(mode) {
}
function updateP1P2Display(mode) {
- const preset = mode === 'sim' ? {selP1: 0x00, selP2: 0x00} : {selP1: 0x09, selP2: 0x0C};
+ const preset = mode === 'sim' ? {selP1: 0x00, selP2: 0x00} : {selP1: 0x00, selP2: 0x0C};
const cmd = document.getElementById(mode + '-cmd').value;
if (cmd === 'select') {
document.getElementById(mode + '-p1').value = preset.selP1.toString(16).padStart(2, '0').toUpperCase();
@@ -1143,13 +1148,14 @@ function updateRamPriv() {
function updateRamToolkit() {
const checked = document.getElementById('ram-toolkit-enable').checked;
document.getElementById('ram-toolkit-body').style.display = checked ? '' : 'none';
- document.getElementById('ram-tk-ad-row').style.display =
- document.getElementById('ram-tk-mode').value === 'ca' ? '' : 'none';
+ updateRamTkMode();
}
-document.getElementById('ram-tk-mode').addEventListener('change', () => {
- document.getElementById('ram-tk-ad-row').style.display =
- document.getElementById('ram-tk-mode').value === 'ca' ? '' : 'none';
-});
+function updateRamTkMode() {
+ const isSim = document.getElementById('ram-tk-mode').value === 'ca';
+ document.getElementById('ram-tk-ad-row').style.display = isSim ? '' : 'none';
+ document.getElementById('ram-tk-services-row').style.display = isSim ? 'none' : '';
+}
+document.getElementById('ram-tk-mode').addEventListener('change', updateRamTkMode);
// ===== GSM 7-bit alphabet =====
const GSM7_ALPHABET = [
@@ -1372,7 +1378,7 @@ function buildSelectApdu(mode, preset, selInfo) {
}
function genSimUsim(mode) {
- const preset = mode === 'sim' ? {cla: 0xA0, selP1: 0x00, selP2: 0x00} : {cla: 0x00, selP1: 0x09, selP2: 0x0C};
+ const preset = mode === 'sim' ? {cla: 0xA0, selP1: 0x00, selP2: 0x00} : {cla: 0x00, selP1: 0x00, selP2: 0x0C};
const cmd = document.getElementById(mode + '-cmd').value;
const doSelect = document.getElementById(mode + '-select').checked;
const selInfo = getSelValue(mode);
@@ -1506,33 +1512,39 @@ function genRam() {
const channels = parseInt(document.getElementById('ram-tk-channels').value) || 0;
const msl = document.getElementById('ram-tk-msl').value;
const tar = (document.getElementById('ram-tk-tar').value || '').replace(/[^0-9a-fA-F]/g, '');
- const ad = (document.getElementById('ram-tk-ad').value || '').replace(/[^0-9a-fA-F]/g, '').padStart(2, '0').slice(0, 2);
+ const adRaw = (document.getElementById('ram-tk-ad').value || '').replace(/[^0-9a-fA-F]/g, '');
+ const ad = adRaw.padStart(2, '0').slice(0, 2);
+ const services = parseInt(document.getElementById('ram-tk-services').value) || 0;
+ let menuPairs = '';
+ for (let i = 1; i <= menus; i++) {
+ if (i === 1) menuPairs += firstPos.toString(16).padStart(2, '0') + firstId;
+ else if (i === menus) menuPairs += lastPos.toString(16).padStart(2, '0') + lastId;
+ else menuPairs += '0000';
+ }
let tkPayload = '';
if (tkMode === 'ca') {
- tkPayload = priority.toString(16).padStart(2, '0') +
+ tkPayload = (adRaw ? '01' + ad : '00') +
+ priority.toString(16).padStart(2, '0') +
timers.toString(16).padStart(2, '0') +
textLen.toString(16).padStart(2, '0') +
menus.toString(16).padStart(2, '0') +
- firstPos.toString(16).padStart(2, '0') + firstId +
- lastPos.toString(16).padStart(2, '0') + lastId +
+ menuPairs +
channels.toString(16).padStart(2, '0') +
'01' + msl +
- (tar ? (tar.length / 2).toString(16).padStart(2, '0') + tar : '00') +
- '01' + ad;
- const tkLen = (tkPayload.length / 2).toString(16);
- dataField += 'CA' + tkLen + tkPayload;
+ (tar ? (tar.length / 2).toString(16).padStart(2, '0') + tar : '00');
+ dataField += 'CA' + berLenStr(tkPayload.length / 2) + tkPayload;
} else {
tkPayload = priority.toString(16).padStart(2, '0') +
timers.toString(16).padStart(2, '0') +
textLen.toString(16).padStart(2, '0') +
menus.toString(16).padStart(2, '0') +
- firstPos.toString(16).padStart(2, '0') + firstId +
- lastPos.toString(16).padStart(2, '0') + lastId +
+ menuPairs +
channels.toString(16).padStart(2, '0') +
'01' + msl +
- (tar ? (tar.length / 2).toString(16).padStart(2, '0') + tar : '00');
- const tkLen = (tkPayload.length / 2).toString(16);
- dataField += '80' + tkLen + tkPayload;
+ (tar ? (tar.length / 2).toString(16).padStart(2, '0') + tar : '00') +
+ services.toString(16).padStart(2, '0');
+ const innerTlv = '80' + berLenStr(tkPayload.length / 2) + tkPayload;
+ dataField += 'EA' + berLenStr(innerTlv.length / 2) + innerTlv;
}
}
const dataLen = (dataField.length / 2).toString(16).padStart(2, '0');
@@ -1552,13 +1564,13 @@ function genRam() {
} else if (cmd === 'load') {
if (!ramData) { resultEl.value = 'Error: specify data'; return; }
const dataLen = (ramData.length / 2).toString(16).padStart(2, '0');
- apdu = buildApdu(0x80, 0xE8, 0x00, blockNum, parseInt(dataLen, 16), ramData);
+ apdu = buildApdu(0x80, 0xE8, 0x80, blockNum, parseInt(dataLen, 16), ramData);
} else if (cmd === 'delete') {
if (!aid) { resultEl.value = 'Error: specify AID'; return; }
const aidLen = (aid.length / 2).toString(16).padStart(2, '0');
const dataField = '4F' + aidLen + aid;
const dataLen = (dataField.length / 2).toString(16).padStart(2, '0');
- apdu = buildApdu(0x80, 0xE4, parseInt(delMode, 16), 0x00, parseInt(dataLen, 16), dataField);
+ apdu = buildApdu(0x80, 0xE4, 0x00, parseInt(delMode, 16), parseInt(dataLen, 16), dataField);
} else if (cmd === 'get-status') {
const gsP2 = document.getElementById('ram-gs-p2').value;
apdu = buildApdu(0x80, 0xF2, parseInt(gsMode, 16), parseInt(gsP2, 16), 0x02, '4F00');
@@ -4882,6 +4894,7 @@ const LANG_RU = {
'Byte 2': 'Байт 2',
'SIM/UICC Toolkit parameters': 'Параметры SIM/UICC Toolkit',
'Toolkit mode': 'Режим Toolkit',
+ 'Max services': 'Максимум сервисов',
'Priority': 'Приоритет',
'Timers (max)': 'Таймеры (макс)',
'Menu text length': 'Длина текста меню',
diff --git a/frontend/sw.js b/frontend/sw.js
index e3b50f1..eac5e3c 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'otaman-v8';
+const CACHE = 'otaman-v9';
const URLS = [
'index.html',
'help.html',
diff --git a/frontend/tests/ram.test.js b/frontend/tests/ram.test.js
new file mode 100644
index 0000000..7a8fbd2
--- /dev/null
+++ b/frontend/tests/ram.test.js
@@ -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);
+ }
+});
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 46faa14..6413756 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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.
diff --git a/pysim_otaman_server/server.py b/pysim_otaman_server/server.py
index b3bf8e6..8ed9015 100644
--- a/pysim_otaman_server/server.py
+++ b/pysim_otaman_server/server.py
@@ -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 /frontend, served by this server