Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 71d2e80242 | |||
| 7870682f3d | |||
| 0fd91d7771 | |||
| 288c2e5954 |
+1
-1
@@ -54,7 +54,7 @@ Returns server version for compatibility checking.
|
||||
|
||||
**Example response:**
|
||||
```json
|
||||
{"version": "1.8.1"}
|
||||
{"version": "1.9.1"}
|
||||
```
|
||||
|
||||
### `GET /api/status`
|
||||
|
||||
+531
-228
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'otaman-v9';
|
||||
const CACHE = 'otaman-v11';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -170,7 +170,161 @@ test('gsm7Decode unpacks "HI" from C824', () => {
|
||||
assert.strictEqual(gsm7Decode(bytes), 'HI');
|
||||
});
|
||||
|
||||
test('decodeTextData legacy UCS2 without DCS (regression: lead 00 is not a DCS)', () => {
|
||||
assert.strictEqual(decodeTextData('00480049'), 'HI');
|
||||
});
|
||||
|
||||
test('decodeTextData legacy Cyrillic UCS2 (uniform 04 high bytes)', () => {
|
||||
assert.strictEqual(decodeTextData('041f04400438043204350442'), 'Привет');
|
||||
});
|
||||
|
||||
test('decodeTextData DCS-prefixed UCS2 (08)', () => {
|
||||
assert.strictEqual(decodeTextData('0800480049'), 'HI');
|
||||
});
|
||||
|
||||
test('decodeTextData DCS-prefixed GSM7 packed (00)', () => {
|
||||
assert.strictEqual(decodeTextData('00C824'), 'HI');
|
||||
});
|
||||
|
||||
test('decodeTextData DCS-prefixed unpacked 8-bit (04)', () => {
|
||||
assert.strictEqual(decodeTextData('04414243'), 'ABC');
|
||||
});
|
||||
|
||||
test('decodeTextData explicit DCS 08 with malformed payload returns ?', () => {
|
||||
assert.strictEqual(decodeTextData('0841'), '?');
|
||||
});
|
||||
|
||||
test('decodePrivileges byte 2 b6 is Token Verification per GPC v2.3 Table 11-8', () => {
|
||||
assert.ok(decodePrivileges('0020').includes('Token Verification'));
|
||||
});
|
||||
|
||||
test('decodePrivileges', () => {
|
||||
assert.ok(decodePrivileges('00').includes('None'));
|
||||
assert.ok(decodePrivileges('80').includes('Security Domain'));
|
||||
});
|
||||
|
||||
test('LV INSTALL [for install] decode with labeled fields and trailing Le', () => {
|
||||
const tree = parseHexTree('80E60C0011000008A000000151000000010002C9000000');
|
||||
const apdu = tree.children[0];
|
||||
assert.ok(findNode(apdu, 'P1').desc.includes('for install + for make selectable'));
|
||||
const elf = findNode(apdu, 'ELF AID');
|
||||
assert.ok(elf);
|
||||
assert.strictEqual(elf.desc, '(empty)');
|
||||
assert.strictEqual(findNode(apdu, 'Application AID').desc, 'A000000151000000');
|
||||
assert.strictEqual(findNode(apdu, 'Privileges').desc, 'None');
|
||||
const params = findNode(apdu, 'Install parameters');
|
||||
assert.ok(params.children.some(c => c.label.includes('C9')));
|
||||
assert.strictEqual(findNode(apdu, 'Install token').desc, '(empty)');
|
||||
assert.strictEqual(tree.children[1].label, 'Le');
|
||||
});
|
||||
|
||||
test('Legacy TLV INSTALL falls back to raw Data node', () => {
|
||||
const tree = parseHexTree('80E60C00214F08A000000151000000C70100EA13801100000002010102020002011603B0000100');
|
||||
const apdu = tree.children[0];
|
||||
const data = findNodes(apdu, 'Data');
|
||||
assert.strictEqual(data.length, 1);
|
||||
assert.ok(!data[0].children || !data[0].label.includes('ELF'));
|
||||
assert.ok(findNode(apdu, 'P1').desc.includes('for install + for make selectable'));
|
||||
});
|
||||
|
||||
test('Install parameters EF nesting exposes inner CA TLV', () => {
|
||||
const tree = parseHexTree('80E60C0025000008A000000151000000010016C900EF12CA1000000000030101000003030002011600' + '00');
|
||||
const apdu = tree.children[0];
|
||||
const params = findNode(apdu, 'Install parameters');
|
||||
assert.ok(params.children.some(c => c.label.includes('EF')));
|
||||
const ef = params.children.find(c => c.label.includes('EF'));
|
||||
assert.ok(ef.children.some(c => c.label.includes('CA')));
|
||||
});
|
||||
|
||||
test('GET DATA case 2 renders P3 as Le', () => {
|
||||
const tree = parseHexTree('80CA5F5000');
|
||||
const apdu = tree.children[0];
|
||||
assert.ok(findNode(apdu, 'Le'));
|
||||
assert.ok(!findNode(apdu, 'Lc'));
|
||||
});
|
||||
|
||||
test('GET DATA case 4 decodes Lc + tag list + Le', () => {
|
||||
const tree = parseHexTree('80CA2F00025C0000');
|
||||
const apdu = tree.children[0];
|
||||
assert.ok(findNode(apdu, 'Lc'));
|
||||
assert.strictEqual(findNode(apdu, 'Data').desc, 'Tag list: 5C00');
|
||||
assert.ok(findNode(apdu, 'Le'));
|
||||
});
|
||||
|
||||
test('SET STATUS raw AID data labeled', () => {
|
||||
const tree = parseHexTree('80F0408008A000000151000000');
|
||||
const apdu = tree.children[0];
|
||||
assert.strictEqual(findNode(apdu, 'P1').desc, 'Application or SSD');
|
||||
assert.strictEqual(findNode(apdu, 'P2').desc, 'LOCKED');
|
||||
assert.ok(findNode(apdu, 'Data').desc.startsWith('AID (raw):'));
|
||||
});
|
||||
|
||||
test('SET STATUS ISD card state label', () => {
|
||||
const tree = parseHexTree('80F0807F00');
|
||||
const apdu = tree.children[0];
|
||||
assert.strictEqual(findNode(apdu, 'P1').desc, 'ISD');
|
||||
assert.strictEqual(findNode(apdu, 'P2').desc, 'CARD_LOCKED');
|
||||
assert.ok(!findNode(apdu, 'Data'));
|
||||
});
|
||||
|
||||
test('Trailing single byte consumed as Le in compact chain', () => {
|
||||
const tree = parseHexTree('A0A40000026F3BDC0102032B2F2D00');
|
||||
const last = tree.children[tree.children.length - 1];
|
||||
assert.strictEqual(last.label, 'Le');
|
||||
assert.strictEqual(last.hex, '00');
|
||||
});
|
||||
|
||||
test('ACTIVATE FILE case-1 (4 bytes)', () => {
|
||||
const tree = parseHexTree('00440000');
|
||||
const apdu = tree.children[0];
|
||||
assert.strictEqual(apdu.hex, '00440000');
|
||||
assert.strictEqual(apdu.desc, 'no data, no Le');
|
||||
assert.ok(!findNode(apdu, 'P3'));
|
||||
assert.ok(!findNode(apdu, 'Lc'));
|
||||
});
|
||||
|
||||
test('ACTIVATE FILE legacy 5-byte empty-Lc form', () => {
|
||||
const tree = parseHexTree('0044000000');
|
||||
const apdu = tree.children[0];
|
||||
assert.strictEqual(findNode(apdu, 'P3').desc, 'empty Lc (legacy form)');
|
||||
});
|
||||
|
||||
test('ACTIVATE FILE with FID data', () => {
|
||||
const tree = parseHexTree('00440000026F3B');
|
||||
const apdu = tree.children[0];
|
||||
assert.strictEqual(findNode(apdu, 'Data').desc, 'FID 6F3B');
|
||||
});
|
||||
|
||||
test('READ RECORD next mode: P1 ignored note', () => {
|
||||
const tree = parseHexTree('00B2000200');
|
||||
const apdu = tree.children[0];
|
||||
assert.strictEqual(findNode(apdu, 'P1').desc, 'ignored');
|
||||
assert.ok(findNode(apdu, 'P2').desc.includes('next record'));
|
||||
});
|
||||
|
||||
test('UPDATE RECORD absolute mode with record number', () => {
|
||||
const tree = parseHexTree('00DC010404AABBCCDD');
|
||||
const apdu = tree.children[0];
|
||||
assert.strictEqual(findNode(apdu, 'P1').desc, 'record #1');
|
||||
assert.strictEqual(findNode(apdu, 'P2').desc, 'absolute/current');
|
||||
});
|
||||
|
||||
test('SELECT P1/P2 descriptors', () => {
|
||||
const tree = parseHexTree('00A40804047FFF6FC500');
|
||||
const apdu = tree.children[0];
|
||||
assert.strictEqual(findNode(apdu, 'P1').desc, 'path from MF');
|
||||
assert.ok(findNode(apdu, 'P2').desc.includes('FCP'));
|
||||
});
|
||||
|
||||
test('Immediate Action EFRMA reference (01-7F)', () => {
|
||||
const tree = parseHexTree('AA03810105');
|
||||
const row = tree.children[0].children[0];
|
||||
assert.strictEqual(row.label, 'Reference to EFRMA record');
|
||||
assert.strictEqual(row.desc, 'Record 0x05');
|
||||
});
|
||||
|
||||
test('CLA 84-87 labeled GlobalPlatform secure messaging', () => {
|
||||
const tree = parseHexTree('8482030010' + '00112233445566778899AABBCCDDEEFF');
|
||||
const apdu = tree.children[0];
|
||||
assert.strictEqual(findNode(apdu, 'CLA').desc, 'GlobalPlatform (secure messaging)');
|
||||
});
|
||||
@@ -148,14 +148,14 @@ test('genBerPcValue DISPLAY TEXT GSM7 "HI"', () => {
|
||||
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');
|
||||
assert.strictEqual(val, '810E8103012101820281828D0300C824');
|
||||
});
|
||||
|
||||
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');
|
||||
assert.strictEqual(val, '81148103012101820281828D0508004800498402011E');
|
||||
});
|
||||
|
||||
test('genBerPcValue duration disabled omits 84', () => {
|
||||
@@ -208,7 +208,7 @@ test('genErrorActionValue proactive wraps proactive command', () => {
|
||||
row.children[1].children.push(pc);
|
||||
const val = genErrorActionValue(row, '82');
|
||||
// DISPLAY TEXT UCS2 "HI" + 30s inside Error Action
|
||||
assert.strictEqual(val, '82138103012101820281828D04004800498402011E');
|
||||
assert.strictEqual(val, '82148103012101820281828D0508004800498402011E');
|
||||
});
|
||||
|
||||
test('genScriptChainingValue first emits 01', () => {
|
||||
|
||||
@@ -87,7 +87,7 @@ function genRamApdu() {
|
||||
|
||||
test('UICC toolkit nested inside EA (m=2, services 0)', () => {
|
||||
const apdu = genRamApdu();
|
||||
assert.ok(apdu.includes('EA128010000000020101020200011603B0000100'), apdu);
|
||||
assert.ok(apdu.includes('EA13801100000002010102020002011603B0000100'), apdu);
|
||||
});
|
||||
|
||||
test('UICC m=1 emits single pair', () => {
|
||||
@@ -111,7 +111,7 @@ test('UICC m=1 emits single pair', () => {
|
||||
set('ram-tk-ad', '');
|
||||
set('ram-tk-services', '0');
|
||||
const apdu = genRamResult();
|
||||
assert.ok(apdu.includes('EA10800E00000001010100011603B0000100'), apdu);
|
||||
assert.ok(apdu.includes('EA11800F0000000101010002011603B0000100'), apdu);
|
||||
});
|
||||
|
||||
test('UICC m=3 fills middle pair with 0000', () => {
|
||||
@@ -135,7 +135,7 @@ test('UICC m=3 fills middle pair with 0000', () => {
|
||||
set('ram-tk-ad', '');
|
||||
set('ram-tk-services', '0');
|
||||
const apdu = genRamResult();
|
||||
assert.ok(apdu.includes('EA1480120000000301010000030300011603B0000100'), apdu);
|
||||
assert.ok(apdu.includes('EA158013000000030101000003030002011603B0000100'), apdu);
|
||||
});
|
||||
|
||||
test('UICC services 7 appended as final byte', () => {
|
||||
@@ -159,7 +159,7 @@ test('UICC services 7 appended as final byte', () => {
|
||||
set('ram-tk-ad', '');
|
||||
set('ram-tk-services', '7');
|
||||
const apdu = genRamResult();
|
||||
assert.ok(apdu.includes('EA128010000000020101020200011603B0000107'), apdu);
|
||||
assert.ok(apdu.includes('EA13801100000002010102020002011603B0000107'), apdu);
|
||||
});
|
||||
|
||||
test('SIM (CA) access domain FIRST, no services byte', () => {
|
||||
@@ -183,7 +183,7 @@ test('SIM (CA) access domain FIRST, no services byte', () => {
|
||||
set('ram-tk-ad', '5A');
|
||||
set('ram-tk-services', '0');
|
||||
const apdu = genRamResult();
|
||||
assert.ok(apdu.includes('CA11015A000000020101020200011603B00001'), apdu);
|
||||
assert.ok(apdu.includes('EF14CA12015A00000002010102020002011603B00001'), apdu);
|
||||
});
|
||||
|
||||
test('SIM (CA) blank access domain emits length byte 00', () => {
|
||||
@@ -207,7 +207,7 @@ test('SIM (CA) blank access domain emits length byte 00', () => {
|
||||
set('ram-tk-ad', '');
|
||||
set('ram-tk-services', '0');
|
||||
const apdu = genRamResult();
|
||||
assert.ok(apdu.includes('CA1000000000020101020200011603B00001'), apdu);
|
||||
assert.ok(apdu.includes('EF13CA110000000002010102020002011603B00001'), apdu);
|
||||
});
|
||||
|
||||
test('SIM (CA) m=3, no TAR, blank access domain', () => {
|
||||
@@ -231,7 +231,7 @@ test('SIM (CA) m=3, no TAR, blank access domain', () => {
|
||||
set('ram-tk-ad', '');
|
||||
set('ram-tk-services', '0');
|
||||
const apdu = genRamResult();
|
||||
assert.ok(apdu.includes('CA0F000000000301010000030300011600'), apdu);
|
||||
assert.ok(apdu.includes('EF12CA1000000000030101000003030002011600'), apdu);
|
||||
});
|
||||
|
||||
test('UICC m=60 uses long-form BER lengths (EA 81 87 / inner 81 84)', () => {
|
||||
@@ -255,7 +255,7 @@ test('UICC m=60 uses long-form BER lengths (EA 81 87 / inner 81 84)', () => {
|
||||
set('ram-tk-ad', '');
|
||||
set('ram-tk-services', '0');
|
||||
const apdu = genRamResult();
|
||||
assert.ok(apdu.includes('EA8187808184'), apdu);
|
||||
assert.ok(apdu.includes('EA8188808185'), apdu);
|
||||
});
|
||||
|
||||
test('LOAD P1 fixed to 80 (last block)', () => {
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-otaman-server"
|
||||
version = "1.8.1"
|
||||
version = "1.9.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.
|
||||
|
||||
@@ -18,7 +18,7 @@ from osmocom.construct import GsmOrUcs2Adapter
|
||||
from osmocom.tlv import BER_TLV_IE
|
||||
|
||||
|
||||
VERSION = '1.8.1'
|
||||
VERSION = '1.9.1'
|
||||
|
||||
|
||||
# Static file serving (the PWA lives in <repo>/frontend, served by this server
|
||||
|
||||
Reference in New Issue
Block a user