From e6602c40bfb4c23a72973b8511538f4d94cadb9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Thu, 20 Aug 2026 21:23:37 +0300 Subject: [PATCH] RAM constructor fixes: EA/CA nesting, access domain order, menu pairs, BER lengths, services byte (TS 102 226) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 ...) - 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 --- docs/api.md | 2 +- frontend/index.html | 71 ++++---- frontend/sw.js | 2 +- frontend/tests/ram.test.js | 293 ++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- pysim_otaman_server/server.py | 2 +- 6 files changed, 339 insertions(+), 33 deletions(-) create mode 100644 frontend/tests/ram.test.js 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