Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cdd3820064 |
@@ -9,6 +9,15 @@ every change carries a descriptive commit message.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [3.32.1] — 2026-10-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **"Start counter value"** in the outfile import: the counter every created keyset
|
||||||
|
starts from. It defaults to the store's value (`1`); raise it for cards whose
|
||||||
|
counters are already past it. The API takes `start_counter` (1–10 hex digits;
|
||||||
|
a bad value is a 400), and the preview carries the value.
|
||||||
|
|
||||||
## [3.32.0] — 2026-10-21
|
## [3.32.0] — 2026-10-21
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -351,6 +351,10 @@ into a preview.
|
|||||||
- `tar_keyset` — an optional **global default keyset** (1–15) for the TAR
|
- `tar_keyset` — an optional **global default keyset** (1–15) for the TAR
|
||||||
entries that do not name one (the API's shorthand; the PWA sends per-row
|
entries that do not name one (the API's shorthand; the PWA sends per-row
|
||||||
keysets instead).
|
keysets instead).
|
||||||
|
- `start_counter` — the counter **every created keyset starts from** (1–10 hex
|
||||||
|
digits; whitespace stripped, uppercased). Without it the store's default
|
||||||
|
(`1`) applies. A bad value is a `400`; the import stores the protocol's
|
||||||
|
10-digit form.
|
||||||
|
|
||||||
Recognised columns (case-insensitive): `ICCID` (the digits + odd-nibble F
|
Recognised columns (case-insensitive): `ICCID` (the digits + odd-nibble F
|
||||||
pad, the plain digits, spaced digits or the raw nibble-swapped EF hex all
|
pad, the plain digits, spaced digits or the raw nibble-swapped EF hex all
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -371,7 +371,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p class="text-sm mb-3">The <strong>SCP81</strong> column shows whether the preset supplies a usable PSK pair: <strong>✓</strong> (green) — identity and key set; <strong>⚠</strong> (amber) — only one of the two, so the listener ignores the preset; <strong>—</strong> — no PSK. PSK identity and key must be set together. <strong>Add preset</strong> opens the editor with a fresh preset; <strong>Edit</strong> loads the row's preset into it so fields can be changed without re-entering the card; <strong>Save</strong> stores it and returns to the list, <strong>Cancel</strong> discards the edits; <strong>Remove</strong> deletes the row (list only). Presets can be shared with <strong>Export to file</strong> and restored with <strong>Import from file</strong>. The selected card preset auto-fills the Secured Packet form and sets the TAR for the operation (RAM/GP → ISD TAR, SIM RFM → UICC RFM TAR, USIM RFM → ADF RFM TAR); a TAR typed into the Secured Packet form by hand is kept when sending - the counter, keys and the TAR's MSL are still re-read from the preset; the SPI1 is seeded from the TAR's MSL and a lower value warns (the server returns it as a <code class="font-mono text-sm">warning</code> too); edits are pushed into a running SCP81 listener automatically.</p>
|
<p class="text-sm mb-3">The <strong>SCP81</strong> column shows whether the preset supplies a usable PSK pair: <strong>✓</strong> (green) — identity and key set; <strong>⚠</strong> (amber) — only one of the two, so the listener ignores the preset; <strong>—</strong> — no PSK. PSK identity and key must be set together. <strong>Add preset</strong> opens the editor with a fresh preset; <strong>Edit</strong> loads the row's preset into it so fields can be changed without re-entering the card; <strong>Save</strong> stores it and returns to the list, <strong>Cancel</strong> discards the edits; <strong>Remove</strong> deletes the row (list only). Presets can be shared with <strong>Export to file</strong> and restored with <strong>Import from file</strong>. The selected card preset auto-fills the Secured Packet form and sets the TAR for the operation (RAM/GP → ISD TAR, SIM RFM → UICC RFM TAR, USIM RFM → ADF RFM TAR); a TAR typed into the Secured Packet form by hand is kept when sending - the counter, keys and the TAR's MSL are still re-read from the preset; the SPI1 is seeded from the TAR's MSL and a lower value warns (the server returns it as a <code class="font-mono text-sm">warning</code> too); edits are pushed into a running SCP81 listener automatically.</p>
|
||||||
<p class="text-sm mb-3"><strong>Import outfile</strong> imports many cards at once from a card vendor's <em>outfile</em> — the personalization tool's plain-text dump: a banner and a configuration header, then a whitespace-separated <strong>column line</strong> (the tokens include <code class="font-mono text-sm">ICCID</code>; the first token may carry a <code class="font-mono text-sm">var_out:</code>-style prefix) and one row per card. Everything before the column line and every unused column is ignored. Every data row must carry a value for <strong>every declared column</strong> — the format is positional, so an empty value is indistinguishable from a missing one — and a <code class="font-mono text-sm">KICn</code>/<code class="font-mono text-sm">KIDn</code> pair named in the header must have a usable value in every row; a violation makes the whole file invalid (the preview reports the line and the import is refused). The parser reads <code class="font-mono text-sm">ICCID</code> (digits, the digits with an odd-nibble <code class="font-mono text-sm">F</code> pad, spaced digits or the raw EF hex — all normalised to one form), <code class="font-mono text-sm">PIN1</code>/<code class="font-mono text-sm">PIN2</code>/<code class="font-mono text-sm">PUK1</code>/<code class="font-mono text-sm">PUK2</code>, <code class="font-mono text-sm">ADM</code> (the key's hex value, 4–16 hex digits in byte pairs — e.g. <code class="font-mono text-sm">3632333239463337</code>), <code class="font-mono text-sm">PSK</code> (16 bytes; an optional <code class="font-mono text-sm">PSKID</code>/<code class="font-mono text-sm">PSK_ID</code> names its identity, defaulting to the card's ICCID) and the numbered key pairs <code class="font-mono text-sm">KICn</code>/<code class="font-mono text-sm">KIDn</code> — one keyset per pair, up to 15. The <strong>KIc/KID bytes are never in the file</strong>: the keyset number is the column number (<code class="font-mono text-sm">KIC2</code> → keyset 2) and the algorithm nibble follows the key size — 8 bytes DES, 24 bytes 3DES3, and for <strong>16 bytes</strong> the preview's choice, <strong>3DES2</strong> (default) or <strong>AES</strong>; any other size skips that keyset with a note. The TARs never come from the file either: the preview shows the TAR table the imported cards get (the three role entries with their spec TARs and MSL 16 by default, editable, with <strong>+ TAR</strong> for extra entries) and <strong>every row carries its own keyset</strong> — a select over the keysets found in the file (the first one is preselected; a non-zero MSL needs one, marked amber), so each TAR's packets use exactly the keyset you choose. A <strong>common name</strong> is applied to every imported card (empty falls back to the ICCID); <strong>Import mode</strong> chooses <strong>Merge</strong> (default — existing presets are kept, duplicate ICCIDs are skipped and reported) or <strong>Replace</strong> (the store is wiped first). The preview lists the parsed cards (ICCID, keysets, the codes found) and every skipped row/key with its reason before anything is stored.</p>
|
<p class="text-sm mb-3"><strong>Import outfile</strong> imports many cards at once from a card vendor's <em>outfile</em> — the personalization tool's plain-text dump: a banner and a configuration header, then a whitespace-separated <strong>column line</strong> (the tokens include <code class="font-mono text-sm">ICCID</code>; the first token may carry a <code class="font-mono text-sm">var_out:</code>-style prefix) and one row per card. Everything before the column line and every unused column is ignored. Every data row must carry a value for <strong>every declared column</strong> — the format is positional, so an empty value is indistinguishable from a missing one — and a <code class="font-mono text-sm">KICn</code>/<code class="font-mono text-sm">KIDn</code> pair named in the header must have a usable value in every row; a violation makes the whole file invalid (the preview reports the line and the import is refused). The parser reads <code class="font-mono text-sm">ICCID</code> (digits, the digits with an odd-nibble <code class="font-mono text-sm">F</code> pad, spaced digits or the raw EF hex — all normalised to one form), <code class="font-mono text-sm">PIN1</code>/<code class="font-mono text-sm">PIN2</code>/<code class="font-mono text-sm">PUK1</code>/<code class="font-mono text-sm">PUK2</code>, <code class="font-mono text-sm">ADM</code> (the key's hex value, 4–16 hex digits in byte pairs — e.g. <code class="font-mono text-sm">3632333239463337</code>), <code class="font-mono text-sm">PSK</code> (16 bytes; an optional <code class="font-mono text-sm">PSKID</code>/<code class="font-mono text-sm">PSK_ID</code> names its identity, defaulting to the card's ICCID) and the numbered key pairs <code class="font-mono text-sm">KICn</code>/<code class="font-mono text-sm">KIDn</code> — one keyset per pair, up to 15. The <strong>KIc/KID bytes are never in the file</strong>: the keyset number is the column number (<code class="font-mono text-sm">KIC2</code> → keyset 2) and the algorithm nibble follows the key size — 8 bytes DES, 24 bytes 3DES3, and for <strong>16 bytes</strong> the preview's choice, <strong>3DES2</strong> (default) or <strong>AES</strong>; any other size skips that keyset with a note. The TARs never come from the file either: the preview shows the TAR table the imported cards get (the three role entries with their spec TARs and MSL 16 by default, editable, with <strong>+ TAR</strong> for extra entries) and <strong>every row carries its own keyset</strong> — a select over the keysets found in the file (the first one is preselected; a non-zero MSL needs one, marked amber), so each TAR's packets use exactly the keyset you choose. <strong>Start counter value</strong> sets the counter every created keyset starts from (default <code class="font-mono text-sm">1</code> — the store's value; raise it for cards already past it). A <strong>common name</strong> is applied to every imported card (empty falls back to the ICCID); <strong>Import mode</strong> chooses <strong>Merge</strong> (default — existing presets are kept, duplicate ICCIDs are skipped and reported) or <strong>Replace</strong> (the store is wiped first). The preview lists the parsed cards (ICCID, keysets, the codes found) and every skipped row/key with its reason before anything is stored.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="mb-10">
|
<section class="mb-10">
|
||||||
|
|||||||
+14
-4
@@ -1943,7 +1943,7 @@
|
|||||||
// ===== Version =====
|
// ===== Version =====
|
||||||
// Single source of truth for the PWA version: shown in the header and used
|
// Single source of truth for the PWA version: shown in the header and used
|
||||||
// by the server version check in pysimConnect().
|
// by the server version check in pysimConnect().
|
||||||
const SIMPLE_VERSION = '3.32.0';
|
const SIMPLE_VERSION = '3.32.1';
|
||||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||||
|
|
||||||
// ===== view history (the browser Back/Forward buttons) =====
|
// ===== view history (the browser Back/Forward buttons) =====
|
||||||
@@ -15154,7 +15154,7 @@ async function cardsOutfileFile(input) {
|
|||||||
}
|
}
|
||||||
_cardsOutfile = { text: text, file: String(file.name || ''),
|
_cardsOutfile = { text: text, file: String(file.name || ''),
|
||||||
name: String(file.name || '').replace(/\.[^.]+$/, ''),
|
name: String(file.name || '').replace(/\.[^.]+$/, ''),
|
||||||
algorithm16: '3des2', mode: 'merge', preview: null };
|
algorithm16: '3des2', cntr: '1', mode: 'merge', preview: null };
|
||||||
_cardsOutfileTars = 0;
|
_cardsOutfileTars = 0;
|
||||||
cardsOutfileRender();
|
cardsOutfileRender();
|
||||||
await cardsOutfilePreview();
|
await cardsOutfilePreview();
|
||||||
@@ -15177,6 +15177,7 @@ function cardsOutfileControls() {
|
|||||||
const val = id => { const el = document.getElementById(id); return el ? String(el.value) : ''; };
|
const val = id => { const el = document.getElementById(id); return el ? String(el.value) : ''; };
|
||||||
if (document.getElementById('cards-of-name')) o.name = val('cards-of-name').trim();
|
if (document.getElementById('cards-of-name')) o.name = val('cards-of-name').trim();
|
||||||
if (document.getElementById('cards-of-algo')) o.algorithm16 = val('cards-of-algo') || '3des2';
|
if (document.getElementById('cards-of-algo')) o.algorithm16 = val('cards-of-algo') || '3des2';
|
||||||
|
if (document.getElementById('cards-of-cntr')) o.cntr = val('cards-of-cntr').trim().toUpperCase();
|
||||||
if (document.getElementById('cards-of-mode')) {
|
if (document.getElementById('cards-of-mode')) {
|
||||||
o.mode = val('cards-of-mode') === 'replace' ? 'replace' : 'merge';
|
o.mode = val('cards-of-mode') === 'replace' ? 'replace' : 'merge';
|
||||||
}
|
}
|
||||||
@@ -15371,6 +15372,13 @@ function cardsOutfileRender() {
|
|||||||
'<select id="cards-of-mode" class="block mt-0.5 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1 dark:bg-slate-800">' +
|
'<select id="cards-of-mode" class="block mt-0.5 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1 dark:bg-slate-800">' +
|
||||||
'<option value="merge"' + (o.mode === 'replace' ? '' : ' selected') + '>' + esc(t('Merge')) + '</option>' +
|
'<option value="merge"' + (o.mode === 'replace' ? '' : ' selected') + '>' + esc(t('Merge')) + '</option>' +
|
||||||
'<option value="replace"' + (o.mode === 'replace' ? ' selected' : '') + '>' + esc(t('Replace')) + '</option></select></label>';
|
'<option value="replace"' + (o.mode === 'replace' ? ' selected' : '') + '>' + esc(t('Replace')) + '</option></select></label>';
|
||||||
|
// The counter every created keyset starts from (v3.32.1): the store's
|
||||||
|
// default, editable for cards whose counter is already past it.
|
||||||
|
html += '<label class="text-xs text-gray-600 dark:text-slate-400" title="' +
|
||||||
|
esc(t('The counter every created keyset starts from (1-10 hex digits); the store default is 1')) + '">' +
|
||||||
|
esc(t('Start counter value')) +
|
||||||
|
'<input id="cards-of-cntr" class="block mt-0.5 w-24 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1 dark:bg-slate-800 font-mono" maxlength="10" value="' +
|
||||||
|
escHtml(o.cntr || '1') + '" onchange="cardsOutfilePreview()"></label>';
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
html += '<div class="text-xs text-gray-600 dark:text-slate-400 mb-1">' + esc(t('TAR table')) + '</div>';
|
html += '<div class="text-xs text-gray-600 dark:text-slate-400 mb-1">' + esc(t('TAR table')) + '</div>';
|
||||||
html += '<div id="cards-of-tars">' + cardsOutfileTarRowsHtml() + '</div>';
|
html += '<div id="cards-of-tars">' + cardsOutfileTarRowsHtml() + '</div>';
|
||||||
@@ -15393,7 +15401,7 @@ async function cardsOutfilePreview() {
|
|||||||
cardsOutfileControls();
|
cardsOutfileControls();
|
||||||
const body = {
|
const body = {
|
||||||
text: o.text, name: o.name, algorithm16: o.algorithm16,
|
text: o.text, name: o.name, algorithm16: o.algorithm16,
|
||||||
tars: cardsOutfileTarsFromForm(),
|
tars: cardsOutfileTarsFromForm(), start_counter: o.cntr,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
o.preview = await pysimFetchOk('/api/presets/import-outfile', body);
|
o.preview = await pysimFetchOk('/api/presets/import-outfile', body);
|
||||||
@@ -15418,7 +15426,7 @@ async function cardsOutfileImport() {
|
|||||||
if (btn) btn.disabled = true;
|
if (btn) btn.disabled = true;
|
||||||
const body = {
|
const body = {
|
||||||
text: o.text, name: o.name, algorithm16: o.algorithm16,
|
text: o.text, name: o.name, algorithm16: o.algorithm16,
|
||||||
tars: cardsOutfileTarsFromForm(), mode: o.mode,
|
tars: cardsOutfileTarsFromForm(), start_counter: o.cntr, mode: o.mode,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const res = await pysimFetchOk('/api/presets/import-outfile', body);
|
const res = await pysimFetchOk('/api/presets/import-outfile', body);
|
||||||
@@ -24012,6 +24020,8 @@ const LANG_RU = {
|
|||||||
'Common name': 'Общее имя',
|
'Common name': 'Общее имя',
|
||||||
'16-byte keys': '16-байтные ключи',
|
'16-byte keys': '16-байтные ключи',
|
||||||
'This TAR needs a keyset (its MSL requires security)': 'Этому TAR нужен keyset (его MSL требует защиты)',
|
'This TAR needs a keyset (its MSL requires security)': 'Этому TAR нужен keyset (его MSL требует защиты)',
|
||||||
|
'Start counter value': 'Начальное значение счётчика',
|
||||||
|
'The counter every created keyset starts from (1-10 hex digits); the store default is 1': 'Счётчик, с которого начинается каждый созданный keyset (1–10 hex-цифр); по умолчанию в хранилище — 1',
|
||||||
'The file is invalid - fix it and refresh the preview': 'Файл некорректен — исправьте его и обновите предпросмотр',
|
'The file is invalid - fix it and refresh the preview': 'Файл некорректен — исправьте его и обновите предпросмотр',
|
||||||
'Import mode': 'Режим импорта',
|
'Import mode': 'Режим импорта',
|
||||||
'Merge': 'Дополнить',
|
'Merge': 'Дополнить',
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simple-v458';
|
const CACHE = 'simple-v459';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -113,8 +113,8 @@ test('the preview and the import both post the outfile endpoint', () => {
|
|||||||
// the keysets travel per TAR row (v3.29.1): no global tar_keyset
|
// the keysets travel per TAR row (v3.29.1): no global tar_keyset
|
||||||
assert.ok(!preview.includes('tar_keyset'), 'the preview must not send a global keyset');
|
assert.ok(!preview.includes('tar_keyset'), 'the preview must not send a global keyset');
|
||||||
assert.ok(!imp.includes('tar_keyset'), 'the import must not send a global keyset');
|
assert.ok(!imp.includes('tar_keyset'), 'the import must not send a global keyset');
|
||||||
assert.match(preview, /tars: cardsOutfileTarsFromForm\(\)/);
|
assert.match(preview, /tars: cardsOutfileTarsFromForm\(\), start_counter: o\.cntr/);
|
||||||
assert.match(imp, /tars: cardsOutfileTarsFromForm\(\), mode: o\.mode/);
|
assert.match(imp, /tars: cardsOutfileTarsFromForm\(\), start_counter: o\.cntr, mode: o\.mode/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the 16-byte algorithm values match the server map', () => {
|
test('the 16-byte algorithm values match the server map', () => {
|
||||||
@@ -225,10 +225,12 @@ test('cardsOutfileControls reads the panel controls back into the state', () =>
|
|||||||
setEl('cards-of-name', ' Batch 7 ');
|
setEl('cards-of-name', ' Batch 7 ');
|
||||||
setEl('cards-of-algo', 'aes');
|
setEl('cards-of-algo', 'aes');
|
||||||
setEl('cards-of-mode', 'replace');
|
setEl('cards-of-mode', 'replace');
|
||||||
|
setEl('cards-of-cntr', ' 1a ');
|
||||||
app.cardsOutfileControls();
|
app.cardsOutfileControls();
|
||||||
assert.strictEqual(app.state.name, 'Batch 7');
|
assert.strictEqual(app.state.name, 'Batch 7');
|
||||||
assert.strictEqual(app.state.algorithm16, 'aes');
|
assert.strictEqual(app.state.algorithm16, 'aes');
|
||||||
assert.strictEqual(app.state.mode, 'replace');
|
assert.strictEqual(app.state.mode, 'replace');
|
||||||
|
assert.strictEqual(app.state.cntr, '1A'); // the store's uppercase hex form
|
||||||
});
|
});
|
||||||
|
|
||||||
test('picking a file previews it with the file name and the TAR defaults', async () => {
|
test('picking a file previews it with the file name and the TAR defaults', async () => {
|
||||||
@@ -254,6 +256,10 @@ test('picking a file previews it with the file name and the TAR defaults', async
|
|||||||
assert.match(els['cards-outfile-panel'].innerHTML, /id="cards-of-tar-0-keyset"/);
|
assert.match(els['cards-outfile-panel'].innerHTML, /id="cards-of-tar-0-keyset"/);
|
||||||
assert.match(els['cards-outfile-panel'].innerHTML, /<option value="1" selected>Keyset 1 — 15\/15<\/option>/);
|
assert.match(els['cards-outfile-panel'].innerHTML, /<option value="1" selected>Keyset 1 — 15\/15<\/option>/);
|
||||||
assert.match(els['cards-outfile-panel'].innerHTML, /8970119000004600098/);
|
assert.match(els['cards-outfile-panel'].innerHTML, /8970119000004600098/);
|
||||||
|
// the start counter field defaults to the store's value and travels in the body
|
||||||
|
assert.match(els['cards-outfile-panel'].innerHTML, /id="cards-of-cntr"/);
|
||||||
|
assert.match(els['cards-outfile-panel'].innerHTML, /value="1"/);
|
||||||
|
assert.strictEqual(body.start_counter, '1');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a failing preview shows the server message in the panel', async () => {
|
test('a failing preview shows the server message in the panel', async () => {
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pysim-simple-server"
|
name = "pysim-simple-server"
|
||||||
version = "3.32.0"
|
version = "3.32.1"
|
||||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||||
license = { text = "GPL-2.0-or-later" }
|
license = { text = "GPL-2.0-or-later" }
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
|
|||||||
@@ -184,13 +184,23 @@ def _hex_value(tok):
|
|||||||
return v if v and len(v) % 2 == 0 else ''
|
return v if v and len(v) % 2 == 0 else ''
|
||||||
|
|
||||||
|
|
||||||
def parse_outfile(text, name='', algorithm16='3des2', tars=None, tar_keyset=None):
|
def parse_outfile(text, name='', algorithm16='3des2', tars=None, tar_keyset=None,
|
||||||
|
start_counter=None):
|
||||||
"""Parse an outfile into preset dicts (the store's shape).
|
"""Parse an outfile into preset dicts (the store's shape).
|
||||||
|
|
||||||
Returns ``{'presets': [...], 'report': {...}}``. Every skipped row or key
|
``start_counter`` (1-10 hex digits) is written into every created keyset -
|
||||||
is named in the report (up to ``SKIP_REPORT_MAX`` lines, then counted) -
|
the counter an operation against that keyset starts from; without it the
|
||||||
nothing is silently dropped. Raises ValueError on a malformed TAR table.
|
store's default ('1') applies. Returns
|
||||||
|
``{'presets': [...], 'report': {...}}``. Every skipped row or key is named
|
||||||
|
in the report (up to ``SKIP_REPORT_MAX`` lines, then counted) - nothing is
|
||||||
|
silently dropped. Raises ValueError on a malformed TAR table or counter.
|
||||||
"""
|
"""
|
||||||
|
counter = None
|
||||||
|
if start_counter not in (None, ''):
|
||||||
|
counter = re.sub(r'\s', '', str(start_counter)).upper()
|
||||||
|
if not re.fullmatch(r'[0-9A-F]{1,10}', counter):
|
||||||
|
raise ValueError('start_counter must be 1-10 hex digits: %r'
|
||||||
|
% (start_counter,))
|
||||||
columns, line_index = find_columns(text)
|
columns, line_index = find_columns(text)
|
||||||
report = {'columns': columns or [], 'header_lines': max(line_index, 0),
|
report = {'columns': columns or [], 'header_lines': max(line_index, 0),
|
||||||
'rows': 0, 'cards': 0, 'keysets': [], 'tars': 0,
|
'rows': 0, 'cards': 0, 'keysets': [], 'tars': 0,
|
||||||
@@ -256,8 +266,11 @@ def parse_outfile(text, name='', algorithm16='3des2', tars=None, tar_keyset=None
|
|||||||
% (row_no, n, len(kic_key) // 2))
|
% (row_no, n, len(kic_key) // 2))
|
||||||
continue
|
continue
|
||||||
byte = '%02X' % ((n << 4) | algo)
|
byte = '%02X' % ((n << 4) | algo)
|
||||||
keysets.append({'kic': byte, 'kid': byte,
|
keyset = {'kic': byte, 'kid': byte,
|
||||||
'kicKey': kic_key, 'kidKey': kid_key})
|
'kicKey': kic_key, 'kidKey': kid_key}
|
||||||
|
if counter is not None:
|
||||||
|
keyset['cntr'] = counter
|
||||||
|
keysets.append(keyset)
|
||||||
if not keysets:
|
if not keysets:
|
||||||
skip('line %d: no usable keyset' % row_no)
|
skip('line %d: no usable keyset' % row_no)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from osmocom.tlv import BER_TLV_IE
|
|||||||
from osmocom.utils import rpad
|
from osmocom.utils import rpad
|
||||||
|
|
||||||
|
|
||||||
VERSION = '3.32.0'
|
VERSION = '3.32.1'
|
||||||
|
|
||||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||||
|
|
||||||
@@ -11610,7 +11610,8 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
name=str(body.get('name') or ''),
|
name=str(body.get('name') or ''),
|
||||||
algorithm16=algorithm16,
|
algorithm16=algorithm16,
|
||||||
tars=tars,
|
tars=tars,
|
||||||
tar_keyset=tar_keyset)
|
tar_keyset=tar_keyset,
|
||||||
|
start_counter=body.get('start_counter'))
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
resp = {'error': str(e)}
|
resp = {'error': str(e)}
|
||||||
self._send_json(resp, 400)
|
self._send_json(resp, 400)
|
||||||
|
|||||||
@@ -979,6 +979,26 @@ class PresetStoreHttpTests(unittest.TestCase):
|
|||||||
self.assertEqual(status, 400, resp)
|
self.assertEqual(status, 400, resp)
|
||||||
self.assertIn('the file is invalid', resp['error'])
|
self.assertIn('the file is invalid', resp['error'])
|
||||||
self.assertEqual(len(self.store.list()), before)
|
self.assertEqual(len(self.store.list()), before)
|
||||||
|
# the import form's start counter (v3.32.1): every created keyset
|
||||||
|
# carries it; an unusable value is a 400
|
||||||
|
status, resp = self._post('/api/presets/import-outfile', {
|
||||||
|
'text': text, 'start_counter': '1A'})
|
||||||
|
self.assertEqual(status, 200, resp)
|
||||||
|
self.assertEqual([k.get('cntr') for k in resp['presets'][0]['keysets']],
|
||||||
|
['1A'] * len(resp['presets'][0]['keysets']))
|
||||||
|
status, resp = self._post('/api/presets/import-outfile', {
|
||||||
|
'text': text, 'start_counter': 'XYZ'})
|
||||||
|
self.assertEqual(status, 400, resp)
|
||||||
|
self.assertIn('start_counter', resp['error'])
|
||||||
|
# ...and an import stores the protocol's padded form (a fresh ICCID:
|
||||||
|
# merge mode skips a duplicate)
|
||||||
|
fresh = text.replace('8970119000004600098', '8970119000004600999')
|
||||||
|
status, resp = self._post('/api/presets/import-outfile', {
|
||||||
|
'text': fresh, 'start_counter': '1A', 'mode': 'merge'})
|
||||||
|
self.assertEqual(status, 200, resp)
|
||||||
|
got = [p for p in self.store.list()
|
||||||
|
if p['iccid'] == '8970119000004600999']
|
||||||
|
self.assertEqual([k['cntr'] for k in got[0]['keysets']], ['000000001A'])
|
||||||
# a non-list TAR table is the caller's error, never a silent fallback
|
# a non-list TAR table is the caller's error, never a silent fallback
|
||||||
status, resp = self._post('/api/presets/import-outfile', {
|
status, resp = self._post('/api/presets/import-outfile', {
|
||||||
'text': text, 'tars': 'nope'})
|
'text': text, 'tars': 'nope'})
|
||||||
|
|||||||
@@ -219,6 +219,26 @@ class OptionTests(unittest.TestCase):
|
|||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
outfile.parse_outfile(text, tars=[{'tar': '000000', 'msl': '16', 'keyset': 'x'}])
|
outfile.parse_outfile(text, tars=[{'tar': '000000', 'msl': '16', 'keyset': 'x'}])
|
||||||
|
|
||||||
|
def test_the_start_counter_is_written_into_every_keyset(self):
|
||||||
|
"""The import form's "Start counter value": every created keyset
|
||||||
|
carries it (the store pads it to the protocol's 10-digit form); without
|
||||||
|
it the store's default ('1') applies. An unusable value is the
|
||||||
|
caller's error, never a silent default (v3.32.1)."""
|
||||||
|
res = outfile.parse_outfile(alfa_text(), start_counter='1a')
|
||||||
|
ks = res['presets'][0]['keysets']
|
||||||
|
self.assertGreaterEqual(len(ks), 2)
|
||||||
|
self.assertEqual([k.get('cntr') for k in ks], ['1A'] * len(ks))
|
||||||
|
# normalised: whitespace stripped, uppercase
|
||||||
|
res = outfile.parse_outfile(alfa_text(), start_counter=' a1 ')
|
||||||
|
self.assertEqual(res['presets'][0]['keysets'][0]['cntr'], 'A1')
|
||||||
|
# unset: no field at all - the store fills its default
|
||||||
|
res = outfile.parse_outfile(alfa_text())
|
||||||
|
self.assertNotIn('cntr', res['presets'][0]['keysets'][0])
|
||||||
|
# an unusable value refuses the whole parse
|
||||||
|
for bad in ('XYZ', '1G', '1234567890A', ' '):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
outfile.parse_outfile(alfa_text(), start_counter=bad)
|
||||||
|
|
||||||
def test_a_malformed_tar_table_raises(self):
|
def test_a_malformed_tar_table_raises(self):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
outfile.parse_outfile(alfa_text(), tars=[{'tar': 'ZZ', 'msl': '16'}])
|
outfile.parse_outfile(alfa_text(), tars=[{'tar': 'ZZ', 'msl': '16'}])
|
||||||
|
|||||||
Reference in New Issue
Block a user