C-APDU encoder features + GET STATUS E3 coverage; v1.9.2
SIM/USIM encoder (registry §2.3): - New PIN commands: DISABLE '26' / ENABLE '28' (single FF-padded PIN, Lc=08) and UNBLOCK '2C' (unblock+new, Lc=10); dynamic field labels - ACTIVATE/DEACTIVATE FILE target selection: Current EF (case 1), by FID (P1=00), path from MF (P1=08), path from current DF (P1=09) with Lc+FID/path data form - LANG_RU entries for new labels Tests: - sim.test.js: new stub-DOM genSimUsim harness; exact-hex for all PIN ops, ACTIVATE/DEACTIVATE forms, SELECT Le rules, RECORD P1=00 rule - response_map.test.js: lookupSw wildcards (91XX / 63CX), LIFECYCLE_MAP per GPC v2.3, PRIVILEGE_NAMES vs Tables 11-7/11-8/11-9 GET STATUS E3 template decoding verified already present (TLV_TAG_NAMES + decodeTlvValue cover 4F/9F70/C5/CF/C4/CC/CE/84). Version 1.9.1 -> 1.9.2, SW cache otaman-v12 -> otaman-v13
This commit is contained in:
+1
-1
@@ -54,7 +54,7 @@ Returns server version for compatibility checking.
|
||||
|
||||
**Example response:**
|
||||
```json
|
||||
{"version": "1.9.1"}
|
||||
{"version": "1.9.2"}
|
||||
```
|
||||
|
||||
### `GET /api/status`
|
||||
|
||||
+72
-8
@@ -18,7 +18,7 @@
|
||||
<div class="max-w-7xl mx-auto px-6 py-2">
|
||||
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v1.9.1</span></h1>
|
||||
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v1.9.2</span></h1>
|
||||
<div class="flex items-center gap-4">
|
||||
<button id="install-btn" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700" style="display:none">INSTALL PWA [for offline use]</button>
|
||||
<a href="https://github.com/anttro/otaman" target="_blank" class="text-xs text-gray-400 hover:text-gray-600 dark:text-slate-500 dark:hover:text-slate-300">github</a>
|
||||
@@ -62,6 +62,9 @@
|
||||
<option value="deactivate-file">DEACTIVATE FILE</option>
|
||||
<option value="verify">VERIFY PIN</option>
|
||||
<option value="change">CHANGE PIN</option>
|
||||
<option value="disable">DISABLE PIN</option>
|
||||
<option value="enable">ENABLE PIN</option>
|
||||
<option value="unblock">UNBLOCK PIN</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -121,6 +124,17 @@
|
||||
<input id="sim-offset" onchange="updateP1P2Display('sim')" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="0000" maxlength="4" value="0000">
|
||||
</div>
|
||||
|
||||
<div id="sim-act-row" class="mb-3">
|
||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Target">Target</label>
|
||||
<select id="sim-act-target" onchange="updateActFileInput('sim')" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
|
||||
<option value="current" data-l10n="Current EF (case 1)">Current EF (case 1)</option>
|
||||
<option value="fid" data-l10n="By FID">By FID</option>
|
||||
<option value="mfpath" data-l10n="Path from MF">Path from MF</option>
|
||||
<option value="dfpath" data-l10n="Path from current DF">Path from current DF</option>
|
||||
</select>
|
||||
<input id="sim-act-file" class="mt-1.5 font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="6FC5 / 7FFF6FC5" style="display:none">
|
||||
</div>
|
||||
|
||||
<div id="sim-le-row" class="mb-3">
|
||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Le (hex)</label>
|
||||
<input id="sim-le" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="00" maxlength="2" value="00">
|
||||
@@ -198,6 +212,9 @@
|
||||
<option value="deactivate-file">DEACTIVATE FILE</option>
|
||||
<option value="verify">VERIFY PIN</option>
|
||||
<option value="change">CHANGE PIN</option>
|
||||
<option value="disable">DISABLE PIN</option>
|
||||
<option value="enable">ENABLE PIN</option>
|
||||
<option value="unblock">UNBLOCK PIN</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -257,6 +274,17 @@
|
||||
<input id="usim-offset" onchange="updateP1P2Display('usim')" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="0000" maxlength="4" value="0000">
|
||||
</div>
|
||||
|
||||
<div id="usim-act-row" class="mb-3">
|
||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Target">Target</label>
|
||||
<select id="usim-act-target" onchange="updateActFileInput('usim')" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
|
||||
<option value="current" data-l10n="Current EF (case 1)">Current EF (case 1)</option>
|
||||
<option value="fid" data-l10n="By FID">By FID</option>
|
||||
<option value="mfpath" data-l10n="Path from MF">Path from MF</option>
|
||||
<option value="dfpath" data-l10n="Path from current DF">Path from current DF</option>
|
||||
</select>
|
||||
<input id="usim-act-file" class="mt-1.5 font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="6FC5 / 7FFF6FC5" style="display:none">
|
||||
</div>
|
||||
|
||||
<div id="usim-le-row" class="mb-3">
|
||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Le (hex)</label>
|
||||
<input id="usim-le" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="00" maxlength="2" value="00">
|
||||
@@ -1104,7 +1132,7 @@ const BINARY_OPS = new Set(['update-binary', 'read-binary', 'erase-binary']);
|
||||
const READ_OPS = new Set(['read-record', 'read-binary']);
|
||||
const DATA_OPS = new Set(['update-record', 'update-binary']);
|
||||
const RECSIZE_OPS = new Set(['update-record']);
|
||||
const PIN_OPS = new Set(['verify', 'change']);
|
||||
const PIN_OPS = new Set(['verify', 'change', 'disable', 'enable', 'unblock']);
|
||||
|
||||
function updateSimUsimFields(mode) {
|
||||
if (!mode) return;
|
||||
@@ -1115,9 +1143,15 @@ function updateSimUsimFields(mode) {
|
||||
document.getElementById(mode + '-data-row').style.display = DATA_OPS.has(cmd) ? '' : 'none';
|
||||
document.getElementById(mode + '-recsize-row').style.display = RECSIZE_OPS.has(cmd) ? '' : 'none';
|
||||
document.getElementById(mode + '-pin-row').style.display = PIN_OPS.has(cmd) ? '' : 'none';
|
||||
document.getElementById(mode + '-pin-val-row').style.display = cmd === 'verify' ? '' : 'none';
|
||||
document.getElementById(mode + '-pin-old-row').style.display = cmd === 'change' ? '' : 'none';
|
||||
document.getElementById(mode + '-pin-new-row').style.display = cmd === 'change' ? '' : 'none';
|
||||
const pinValCmds = cmd === 'verify' || cmd === 'disable' || cmd === 'enable';
|
||||
document.getElementById(mode + '-pin-val-row').style.display = pinValCmds ? '' : 'none';
|
||||
const oldNewCmds = cmd === 'change' || cmd === 'unblock';
|
||||
document.getElementById(mode + '-pin-old-row').style.display = oldNewCmds ? '' : 'none';
|
||||
document.getElementById(mode + '-pin-new-row').style.display = oldNewCmds ? '' : 'none';
|
||||
if (oldNewCmds) {
|
||||
document.querySelector('#' + mode + '-pin-old-row label').textContent = cmd === 'unblock' ? 'Unblock PIN' : 'Old PIN';
|
||||
}
|
||||
document.getElementById(mode + '-act-row').style.display = (cmd === 'activate-file' || cmd === 'deactivate-file') ? '' : 'none';
|
||||
if (cmd === 'select') {
|
||||
document.getElementById(mode + '-select-checkbox-row').style.display = 'none';
|
||||
} else {
|
||||
@@ -1135,6 +1169,11 @@ function updateSimSelectMethod(mode) {
|
||||
document.getElementById(mode + '-sel-chain-row').style.display = method === 'chain' ? '' : 'none';
|
||||
}
|
||||
|
||||
function updateActFileInput(mode) {
|
||||
const target = document.getElementById(mode + '-act-target').value;
|
||||
document.getElementById(mode + '-act-file').style.display = target === 'current' ? 'none' : '';
|
||||
}
|
||||
|
||||
function toggleSimOverride(mode) {
|
||||
const checked = document.getElementById(mode + '-override').checked;
|
||||
document.getElementById(mode + '-p1').disabled = !checked;
|
||||
@@ -1178,8 +1217,14 @@ function updateP1P2Display(mode) {
|
||||
} else if (cmd === 'read-binary' || cmd === 'update-binary' || cmd === 'erase-binary') {
|
||||
p1 = (offset >> 8) & 0xFF; p2 = offset & 0xFF;
|
||||
} else if (cmd === 'activate-file' || cmd === 'deactivate-file') {
|
||||
const target = document.getElementById(mode + '-act-target').value;
|
||||
if (target === 'current') {
|
||||
p1 = 0x00; p2 = 0x00;
|
||||
} else if (cmd === 'verify' || cmd === 'change') {
|
||||
} else {
|
||||
p1 = target === 'fid' ? 0x00 : (target === 'mfpath' ? 0x08 : 0x09);
|
||||
p2 = 0x00;
|
||||
}
|
||||
} else if (cmd === 'verify' || cmd === 'change' || cmd === 'disable' || cmd === 'enable' || cmd === 'unblock') {
|
||||
p1 = 0x00; p2 = parseInt(pin, 16);
|
||||
} else {
|
||||
p1 = 0x00; p2 = 0x00;
|
||||
@@ -1417,6 +1462,9 @@ const OPS = {
|
||||
'deactivate-file': {ins: 0x04},
|
||||
'verify': {ins: 0x20},
|
||||
'change': {ins: 0x24},
|
||||
'disable': {ins: 0x26},
|
||||
'enable': {ins: 0x28},
|
||||
'unblock': {ins: 0x2C},
|
||||
};
|
||||
|
||||
function getSelValue(mode) {
|
||||
@@ -1513,13 +1561,23 @@ function genSimUsim(mode) {
|
||||
} else if (cmd === 'erase-binary') {
|
||||
p1 = (offset >> 8) & 0xFF; p2 = offset & 0xFF; p3 = 0x00;
|
||||
} else if (cmd === 'activate-file' || cmd === 'deactivate-file') {
|
||||
const target = document.getElementById(mode + '-act-target').value;
|
||||
if (target === 'current') {
|
||||
p1 = 0x00; p2 = 0x00; p3 = null;
|
||||
} else if (cmd === 'verify') {
|
||||
} else {
|
||||
const fileHex = (document.getElementById(mode + '-act-file').value || '').replace(/[^0-9a-fA-F]/g, '');
|
||||
if (!fileHex || fileHex.length % 2 !== 0) { resultEl.value = 'Error: specify FID or path (even hex length)'; return; }
|
||||
p1 = target === 'fid' ? 0x00 : (target === 'mfpath' ? 0x08 : 0x09);
|
||||
p2 = 0x00;
|
||||
p3 = fileHex.length / 2;
|
||||
data = fileHex;
|
||||
}
|
||||
} else if (cmd === 'verify' || cmd === 'disable' || cmd === 'enable') {
|
||||
const pinVal = document.getElementById(mode + '-pin-val').value || '';
|
||||
if (!pinVal) { resultEl.value = 'Error: specify PIN value'; return; }
|
||||
const pinHex = Array.from(pinVal).map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join('').padEnd(16, 'F').slice(0, 16);
|
||||
p1 = 0x00; p2 = parseInt(pin, 16); p3 = 0x08; data = pinHex;
|
||||
} else if (cmd === 'change') {
|
||||
} else if (cmd === 'change' || cmd === 'unblock') {
|
||||
const oldPin = document.getElementById(mode + '-pin-old').value || '';
|
||||
const newPin = document.getElementById(mode + '-pin-new').value || '';
|
||||
if (!oldPin || !newPin) { resultEl.value = 'Error: specify old and new PIN'; return; }
|
||||
@@ -5716,6 +5774,12 @@ const LANG_RU = {
|
||||
'ELF AID (hex)': 'ELF AID (hex)',
|
||||
'Module AID (hex)': 'AID модуля (hex)',
|
||||
'Security level (P1)': 'Уровень безопасности (P1)',
|
||||
'Target': 'Цель',
|
||||
'Current EF (case 1)': 'Текущий EF (case 1)',
|
||||
'By FID': 'По FID',
|
||||
'Path from MF': 'Путь от MF',
|
||||
'Path from current DF': 'Путь от текущего DF',
|
||||
'Unblock PIN': 'Разблокировка PIN',
|
||||
'Byte 1': 'Байт 1',
|
||||
'Byte 2': 'Байт 2',
|
||||
'SIM/UICC Toolkit parameters': 'Параметры SIM/UICC Toolkit',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'otaman-v12';
|
||||
const CACHE = 'otaman-v13';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
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 extractBlock(startMarker, endMarker) {
|
||||
const start = html.indexOf(startMarker);
|
||||
const end = html.indexOf(endMarker, start);
|
||||
if (start < 0 || end < 0) throw new Error('block not found');
|
||||
return html.slice(start, end);
|
||||
}
|
||||
|
||||
// Response parser maps + lookupSw live between SW_MAP and updateRespCmd.
|
||||
// Rewrite top-level const -> var so the maps leak out of sloppy-mode eval.
|
||||
eval(extractBlock('const SW_MAP = {', 'function updateRespCmd').replace(/^const /gm, 'var '));
|
||||
|
||||
test('lookupSw wildcard 91XX resolves proactive pending (any length)', () => {
|
||||
const desc = lookupSw('91', '14', 'uicc');
|
||||
assert.ok(desc.includes('proactive'), desc);
|
||||
assert.ok(!desc.includes('no response data'));
|
||||
});
|
||||
|
||||
test('lookupSw 63CX extracts retry counter', () => {
|
||||
const desc = lookupSw('63', 'C3', 'uicc');
|
||||
assert.ok(desc.includes('3'), desc);
|
||||
});
|
||||
|
||||
test('SW_MAP.generic covers ISO 6A85/6A89/6A8A', () => {
|
||||
assert.ok(SW_MAP.generic['6A85']);
|
||||
assert.ok(SW_MAP.generic['6A89']);
|
||||
assert.ok(SW_MAP.generic['6A8A']);
|
||||
});
|
||||
|
||||
test('gp 6310 label does not reference removed P2=42 option', () => {
|
||||
assert.ok(!SW_MAP.gp['6310'].includes('42'));
|
||||
});
|
||||
|
||||
test('LIFECYCLE_MAP per GPC v2.3 life cycles', () => {
|
||||
assert.strictEqual(LIFECYCLE_MAP['01'], 'OP_READY (card) / LOADED (ELF)');
|
||||
assert.strictEqual(LIFECYCLE_MAP['03'], 'INSTALLED');
|
||||
assert.strictEqual(LIFECYCLE_MAP['07'], 'SELECTABLE');
|
||||
assert.strictEqual(LIFECYCLE_MAP['0F'], 'SECURED (card)');
|
||||
assert.strictEqual(LIFECYCLE_MAP['1F'], 'PERSONALIZED (SD)');
|
||||
assert.strictEqual(LIFECYCLE_MAP['7F'], 'CARD_LOCKED');
|
||||
assert.strictEqual(LIFECYCLE_MAP['FF'], 'TERMINATED');
|
||||
assert.strictEqual(LIFECYCLE_MAP['83'], 'LOCKED (SD)');
|
||||
});
|
||||
|
||||
test('PRIVILEGE_NAMES matches GPC v2.3 Tables 11-7/11-8/11-9', () => {
|
||||
assert.strictEqual(PRIVILEGE_NAMES[0][7], 'Mandated DAP Verification');
|
||||
assert.strictEqual(PRIVILEGE_NAMES[1][2], 'Token Verification');
|
||||
assert.strictEqual(PRIVILEGE_NAMES[1][7], 'Global Service');
|
||||
assert.strictEqual(PRIVILEGE_NAMES[2][0], 'Receipt Generation');
|
||||
assert.strictEqual(PRIVILEGE_NAMES[2][3], 'Contactless Self-Activation');
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
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 = {};
|
||||
this.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
const els = {};
|
||||
const doc = {
|
||||
getElementById: (id) => {
|
||||
if (!els[id]) els[id] = new StubEl();
|
||||
return els[id];
|
||||
},
|
||||
querySelector: () => new StubEl(),
|
||||
};
|
||||
global.document = doc;
|
||||
|
||||
const FNS = ['buildApdu', 'buildOpNoCla', 'buildSelect', 'getSelValue', 'buildSelectApdu',
|
||||
'updateSimUsimFields', 'updateP1P2Display', 'genSimUsim', 'OPS'];
|
||||
let code = '';
|
||||
for (const f of FNS) {
|
||||
if (f === 'OPS') {
|
||||
const m = html.match(/const OPS = \{[\s\S]*?\n\};/);
|
||||
code += m[0].replace(/^const /, 'var ') + '\n';
|
||||
} else {
|
||||
code += extractFunc(html, f) + '\n';
|
||||
}
|
||||
}
|
||||
code += '\nvar SIM_PRESETS = {};';
|
||||
eval(code);
|
||||
|
||||
function set(id, v) { els[id] = Object.assign(new StubEl(), { value: v }); }
|
||||
function setChecked(id, v) { els[id] = Object.assign(new StubEl(), { checked: v }); }
|
||||
|
||||
function setup(mode, opts) {
|
||||
for (const k of Object.keys(els)) delete els[k];
|
||||
set(mode + '-cmd', opts.cmd);
|
||||
setChecked(mode + '-select', !!opts.doSelect);
|
||||
set(mode + '-sel-method', opts.selMethod || 'fid');
|
||||
set(mode + '-sel-fid', opts.fid || '');
|
||||
set(mode + '-sel-path', opts.path || '');
|
||||
set(mode + '-sel-dfname', opts.dfname || '');
|
||||
set(mode + '-sel-chain', opts.chain || '');
|
||||
set(mode + '-record', opts.record || '1');
|
||||
set(mode + '-rec-mode', opts.recMode || '04');
|
||||
set(mode + '-offset', opts.offset || '0000');
|
||||
set(mode + '-le', opts.le || '00');
|
||||
set(mode + '-data', opts.data || '');
|
||||
set(mode + '-recsize', opts.recsize || '0');
|
||||
set(mode + '-pin', opts.pin || '01');
|
||||
set(mode + '-pin-val', opts.pinVal || '');
|
||||
set(mode + '-pin-old', opts.pinOld || '');
|
||||
set(mode + '-pin-new', opts.pinNew || '');
|
||||
set(mode + '-act-target', opts.actTarget || 'current');
|
||||
set(mode + '-act-file', opts.actFile || '');
|
||||
set(mode + '-override', '');
|
||||
els[mode + '-result'] = new StubEl();
|
||||
els[mode + '-pack-btn'] = new StubEl();
|
||||
genSimUsim(mode);
|
||||
return els[mode + '-result'].value;
|
||||
}
|
||||
|
||||
test('VERIFY PIN FF-pads to 8 bytes (Lc=08)', () => {
|
||||
const apdu = setup('usim', { cmd: 'verify', pin: '01', pinVal: '1234' });
|
||||
assert.strictEqual(apdu, '002000010831323334FFFFFFFF');
|
||||
});
|
||||
|
||||
test('CHANGE PIN emits two 8-byte fields (Lc=10)', () => {
|
||||
const apdu = setup('usim', { cmd: 'change', pin: '01', pinOld: '1234', pinNew: '5678' });
|
||||
assert.strictEqual(apdu, '002400011031323334FFFFFFFF35363738FFFFFFFF');
|
||||
});
|
||||
|
||||
test('DISABLE PIN single padded PIN (INS 26)', () => {
|
||||
const apdu = setup('usim', { cmd: 'disable', pin: '02', pinVal: '9999' });
|
||||
assert.strictEqual(apdu, '002600020839393939FFFFFFFF');
|
||||
});
|
||||
|
||||
test('ENABLE PIN single padded PIN (INS 28)', () => {
|
||||
const apdu = setup('usim', { cmd: 'enable', pin: '01', pinVal: '1111' });
|
||||
assert.strictEqual(apdu, '002800010831313131FFFFFFFF');
|
||||
});
|
||||
|
||||
test('UNBLOCK PIN two padded PINs (INS 2C)', () => {
|
||||
const apdu = setup('usim', { cmd: 'unblock', pin: '02', pinOld: '12345678', pinNew: '4321' });
|
||||
assert.strictEqual(apdu, '002C000210313233343536373834333231FFFFFFFF');
|
||||
});
|
||||
|
||||
test('ACTIVATE FILE case-1 (no data, no Le)', () => {
|
||||
const apdu = setup('usim', { cmd: 'activate-file' });
|
||||
assert.strictEqual(apdu, '00440000');
|
||||
});
|
||||
|
||||
test('DEACTIVATE FILE case-1', () => {
|
||||
const apdu = setup('sim', { cmd: 'deactivate-file' });
|
||||
assert.strictEqual(apdu, 'A0040000');
|
||||
});
|
||||
|
||||
test('ACTIVATE FILE by FID (P1=00 + Lc/FID)', () => {
|
||||
const apdu = setup('usim', { cmd: 'activate-file', actTarget: 'fid', actFile: '6F3B' });
|
||||
assert.strictEqual(apdu, '00440000026F3B');
|
||||
});
|
||||
|
||||
test('ACTIVATE FILE by path from MF (P1=08)', () => {
|
||||
const apdu = setup('usim', { cmd: 'activate-file', actTarget: 'mfpath', actFile: '7FFF6FC5' });
|
||||
assert.strictEqual(apdu, '00440800047FFF6FC5');
|
||||
});
|
||||
|
||||
test('DEACTIVATE FILE by path from current DF (P1=09, SIM CLA)', () => {
|
||||
const apdu = setup('sim', { cmd: 'deactivate-file', actTarget: 'dfpath', actFile: '6F3B' });
|
||||
assert.strictEqual(apdu, 'A0040900026F3B');
|
||||
});
|
||||
|
||||
test('READ RECORD next mode forces P1=00', () => {
|
||||
const apdu = setup('usim', { cmd: 'read-record', recMode: '02', le: '20' });
|
||||
assert.strictEqual(apdu, '00B2000220');
|
||||
});
|
||||
|
||||
test('SELECT by FID USIM: P2=0C, no Le', () => {
|
||||
const apdu = setup('usim', { cmd: 'select', selMethod: 'fid', fid: '6FC5' });
|
||||
assert.strictEqual(apdu, '00A4000C026FC5');
|
||||
});
|
||||
|
||||
test('SELECT by path USIM: P2=04, Le=00', () => {
|
||||
const apdu = setup('usim', { cmd: 'select', selMethod: 'path', path: '7FFF6FC5' });
|
||||
assert.strictEqual(apdu, '00A40804047FFF6FC500');
|
||||
});
|
||||
|
||||
test('SELECT by FID SIM (CLA A0): no Le anywhere', () => {
|
||||
const apdu = setup('sim', { cmd: 'select', selMethod: 'fid', fid: '6FC5' });
|
||||
assert.strictEqual(apdu, 'A0A40000026FC5');
|
||||
});
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-otaman-server"
|
||||
version = "1.9.1"
|
||||
version = "1.9.2"
|
||||
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.9.1'
|
||||
VERSION = '1.9.2'
|
||||
|
||||
|
||||
# Static file serving (the PWA lives in <repo>/frontend, served by this server
|
||||
|
||||
Reference in New Issue
Block a user