ui: verify the ADM PIN from the matched card preset (v2.7.8)
The preset ADM key was stored but never used: the header badge showed whether a key exists and whether the card was verified, yet the only way to verify was the pySim command line. - POST /api/verify-adm builds the TS 102 221 VERIFY itself (CHV number from the card model, short keys padded to 8 bytes with 'f') so the raw SW is reported: 63Cx -> attempts_left, 6983/9804 -> blocked, 6982 -> security error. The key is never stored and is redacted from request logs. - PWA: the header ADM badge is clickable when the matched preset has a key; a failed file-manager read/write (6982/9804) shows a Verify ADM button next to the error. Every retry after a failure asks for confirmation and shows the remaining attempts (stronger text on the last attempt); a blocked ADM disables both entry points until the card session changes. No automatic retries. - tests: tests/test_adm_verify.py (fake scc, APDU/SW mapping, redaction) and frontend/tests/adm_verify.test.js (retry prompt, SW classifier, wiring) + card_state indicator expectations - docs/api.md, help EN/RU, AGENTS; version trio 2.7.8; sw cache v211
This commit is contained in:
+28
@@ -30,6 +30,7 @@ a 2.x PWA).
|
|||||||
| `/api/read` | POST | Read file content |
|
| `/api/read` | POST | Read file content |
|
||||||
| `/api/write` | POST | Write raw hex data to a file |
|
| `/api/write` | POST | Write raw hex data to a file |
|
||||||
| `/api/apdu` | POST | Raw APDU send |
|
| `/api/apdu` | POST | Raw APDU send |
|
||||||
|
| `/api/verify-adm` | POST | Verify the card's ADM PIN (from the matched card preset) |
|
||||||
| `/api/help` | POST | pySim help for a given command |
|
| `/api/help` | POST | pySim help for a given command |
|
||||||
| `/api/send-ota` | POST | SCP80 OTA secured packet delivery |
|
| `/api/send-ota` | POST | SCP80 OTA secured packet delivery |
|
||||||
| `/api/ram-install` | POST | Install a Java Card `.cap` file via SCP80 (INSTALL[for load] → LOAD ×N → INSTALL[for install]) |
|
| `/api/ram-install` | POST | Install a Java Card `.cap` file via SCP80 (INSTALL[for load] → LOAD ×N → INSTALL[for install]) |
|
||||||
@@ -134,6 +135,33 @@ Returns:
|
|||||||
{"response": "...", "sw": "9000"}
|
{"response": "...", "sw": "9000"}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `POST /api/verify-adm`
|
||||||
|
|
||||||
|
Verify the card's ADM PIN (TS 102 221 VERIFY, CHV number from the card
|
||||||
|
model). The key comes from the PWA's matched card preset and is never stored;
|
||||||
|
it is redacted from the request log. Short keys (4-16 hex digits) are padded
|
||||||
|
to the 8 CHV bytes with `f`, like pySim's `verify_adm`. The response is
|
||||||
|
structured so the UI can warn about the remaining attempts before retrying.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"adm": "0011223344556677"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns on success:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"ok": true, "sw": "9000"}
|
||||||
|
```
|
||||||
|
|
||||||
|
On a wrong key (`63Cx`, x attempts left):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"ok": false, "sw": "63C2", "attempts_left": 2}
|
||||||
|
```
|
||||||
|
|
||||||
|
A blocked ADM (`6983`/`9804`) reports `{"ok": false, "sw": "9804",
|
||||||
|
"blocked": true}` and cannot be recovered without the card's unblock key.
|
||||||
|
|
||||||
### `POST /api/help`
|
### `POST /api/help`
|
||||||
|
|
||||||
Get structured help for a shell command.
|
Get structured help for a shell command.
|
||||||
|
|||||||
@@ -356,6 +356,7 @@
|
|||||||
<li><strong>Редактировать</strong> — изменение hex-данных, <strong>Сохранить</strong> для записи (или <strong>Отмена</strong>)</li>
|
<li><strong>Редактировать</strong> — изменение hex-данных, <strong>Сохранить</strong> для записи (или <strong>Отмена</strong>)</li>
|
||||||
<li><strong>Данные как на карте / Декодированные данные</strong> — переключение между hex-дампом и таблицей декодированных полей (клиентские декодеры EF: IMSI, ICCID, SPN, списки PLMN, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); серверный pySim JSON того же чтения остаётся доступен в свёрнутом блоке <em>pySim JSON (сервер)</em></li>
|
<li><strong>Данные как на карте / Декодированные данные</strong> — переключение между hex-дампом и таблицей декодированных полей (клиентские декодеры EF: IMSI, ICCID, SPN, списки PLMN, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); серверный pySim JSON того же чтения остаётся доступен в свёрнутом блоке <em>pySim JSON (сервер)</em></li>
|
||||||
<li><strong>Проверить все файлы</strong> — обход всего дерева (включая пользовательские файлы) с пометкой каждого элемента: есть (обычный вид) или нет (красный ✗, без стрелки разворачивания); существующие пустые DF показывают <code class="font-mono text-sm">(пусто)</code>. Отображается прогресс <em>N / всего</em>, обход можно остановить; в конце — сводка «есть/нет». Файлы проверяются только при разворачивании или проверке — просмотр остаётся ленивым.</li>
|
<li><strong>Проверить все файлы</strong> — обход всего дерева (включая пользовательские файлы) с пометкой каждого элемента: есть (обычный вид) или нет (красный ✗, без стрелки разворачивания); существующие пустые DF показывают <code class="font-mono text-sm">(пусто)</code>. Отображается прогресс <em>N / всего</em>, обход можно остановить; в конце — сводка «есть/нет». Файлы проверяются только при разворачивании или проверке — просмотр остаётся ленивым.</li>
|
||||||
|
<li><strong>ADM</strong> — файлы, требующие администраторский PIN, возвращают <code class="font-mono text-sm">6982</code>/<code class="font-mono text-sm">9804</code>; если в подходящей предустановке карты (тот же ICCID) есть ключ ADM, рядом с ошибкой появляется кнопка <strong>«Проверить ADM»</strong>, а значок в заголовке (<code class="font-mono text-sm">ADM ✓/✗ ⚿</code>) становится кликабельным. Каждый неверный ключ расходует попытку: остаток показывается, а повторная попытка требует подтверждения. Заблокированный ADM здесь не восстановить — нужен ключ разблокировки карты.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3 id="pysim-cmdline" class="text-lg font-medium mb-2">6.2 Командная строка pySim</h3>
|
<h3 id="pysim-cmdline" class="text-lg font-medium mb-2">6.2 Командная строка pySim</h3>
|
||||||
|
|||||||
@@ -356,6 +356,7 @@
|
|||||||
<li><strong>Edit</strong> — modify hex data, <strong>Save</strong> to write back (or <strong>Cancel</strong>)</li>
|
<li><strong>Edit</strong> — modify hex data, <strong>Save</strong> to write back (or <strong>Cancel</strong>)</li>
|
||||||
<li><strong>Raw / Decoded</strong> — toggle between the hex dump and a decoded field table (client-side EF decoders: IMSI, ICCID, SPN, PLMN lists, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, service tables, SUME, …); the server-side pySim JSON of the same read stays available in the collapsed <em>pySim JSON (server)</em> disclosure</li>
|
<li><strong>Raw / Decoded</strong> — toggle between the hex dump and a decoded field table (client-side EF decoders: IMSI, ICCID, SPN, PLMN lists, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, service tables, SUME, …); the server-side pySim JSON of the same read stays available in the collapsed <em>pySim JSON (server)</em> disclosure</li>
|
||||||
<li><strong>Probe all files</strong> — walks the whole tree (including custom files) and marks every entry present (normal) or absent (red ✗, no expand arrow); empty-but-present DFs show <code class="font-mono text-sm">(empty)</code>. Shows progress <em>N / total</em>, can be stopped, and finishes with a present/absent summary. Files are only verified when expanded or probed — browsing stays lazy.</li>
|
<li><strong>Probe all files</strong> — walks the whole tree (including custom files) and marks every entry present (normal) or absent (red ✗, no expand arrow); empty-but-present DFs show <code class="font-mono text-sm">(empty)</code>. Shows progress <em>N / total</em>, can be stopped, and finishes with a present/absent summary. Files are only verified when expanded or probed — browsing stays lazy.</li>
|
||||||
|
<li><strong>ADM</strong> — files that need the administrator PIN fail with <code class="font-mono text-sm">6982</code>/<code class="font-mono text-sm">9804</code>; if the matching card preset (same ICCID) carries an ADM key, a <strong>Verify ADM</strong> button appears next to the error and the header badge (<code class="font-mono text-sm">ADM ✓/✗ ⚿</code>) becomes clickable. Every wrong key consumes an attempt: the remaining attempts are shown and a retry asks for confirmation. A blocked ADM cannot be recovered here — it needs the card’s unblock key.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3 id="pysim-cmdline" class="text-lg font-medium mb-2">6.2 pySim command line</h3>
|
<h3 id="pysim-cmdline" class="text-lg font-medium mb-2">6.2 pySim command line</h3>
|
||||||
|
|||||||
+128
-11
@@ -1413,7 +1413,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 = '2.7.7';
|
const SIMPLE_VERSION = '2.7.8';
|
||||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||||
|
|
||||||
// ===== Tab switching =====
|
// ===== Tab switching =====
|
||||||
@@ -4858,6 +4858,12 @@ let _pysimCardEquipped = false;
|
|||||||
let _pysimEquipping = false;
|
let _pysimEquipping = false;
|
||||||
let _pysimAdmVerified = null; // null = no card session
|
let _pysimAdmVerified = null; // null = no card session
|
||||||
let _pysimAdmKey = null; // ADM key present in the matching preset
|
let _pysimAdmKey = null; // ADM key present in the matching preset
|
||||||
|
let _pysimAdmCanVerify = false; // indicator clickable (card + preset key, not blocked)
|
||||||
|
let _pysimAdmAttemptsLeft = null; // last 63Cx result for this card session
|
||||||
|
let _pysimAdmBlocked = false; // 6983/9804 — no further tries until the session changes
|
||||||
|
let _pysimAdmVerifying = false; // verify request in flight (double-click guard)
|
||||||
|
let _pysimAdmStateKey = null; // last rendered indicator state
|
||||||
|
let _pysimLastStatus = null; // last /api/status payload (for local repaints)
|
||||||
let _pysimCardIccid = null; // EF.ICCID digits of the equipped card (null = unknown)
|
let _pysimCardIccid = null; // EF.ICCID digits of the equipped card (null = unknown)
|
||||||
let _pysimHeaderIccid = undefined; // last value rendered in the header indicator
|
let _pysimHeaderIccid = undefined; // last value rendered in the header indicator
|
||||||
let _pysimHeaderScp80 = undefined; // last SCP80/SCP81 marker state
|
let _pysimHeaderScp80 = undefined; // last SCP80/SCP81 marker state
|
||||||
@@ -4934,17 +4940,24 @@ function pysimApplyAvailability() {
|
|||||||
// Compact ADM state next to the header's card indicator: "ADM ✓" when the
|
// Compact ADM state next to the header's card indicator: "ADM ✓" when the
|
||||||
// administrator PIN was verified (pySim rs.adm_verified), "ADM ✗" otherwise;
|
// administrator PIN was verified (pySim rs.adm_verified), "ADM ✗" otherwise;
|
||||||
// a trailing key glyph (⚿) marks that the matching card preset carries an ADM
|
// a trailing key glyph (⚿) marks that the matching card preset carries an ADM
|
||||||
// key. Hidden without a card session; only rewrites the DOM when the state
|
// key. With a key present the badge is clickable and verifies it on demand;
|
||||||
// changes (the 2s /api/status poll calls this on every update).
|
// hidden without a card session. Only rewrites the DOM when the state changes
|
||||||
|
// (the 2s /api/status poll calls this on every update).
|
||||||
function pysimUpdateAdmIndicator(status) {
|
function pysimUpdateAdmIndicator(status) {
|
||||||
const el = document.getElementById('state-indicator-adm');
|
const el = document.getElementById('state-indicator-adm');
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
const verified = (status && status.connected) ? !!status.adm_verified : null;
|
const verified = (status && status.connected) ? !!status.adm_verified : null;
|
||||||
const key = verified === null ? null : cardsAdmPresent(cardsMatchedPreset());
|
const key = verified === null ? null : cardsAdmPresent(cardsMatchedPreset());
|
||||||
if (verified === _pysimAdmVerified && key === _pysimAdmKey) return;
|
const canVerify = key === true && !_pysimAdmBlocked && !_pysimAdmVerifying;
|
||||||
|
const stateKey = [verified, key, canVerify, _pysimAdmAttemptsLeft, _pysimAdmBlocked].join('|');
|
||||||
|
if (stateKey === _pysimAdmStateKey) return;
|
||||||
|
_pysimAdmStateKey = stateKey;
|
||||||
_pysimAdmVerified = verified;
|
_pysimAdmVerified = verified;
|
||||||
_pysimAdmKey = key;
|
_pysimAdmKey = key;
|
||||||
el.classList.remove('text-emerald-600', 'dark:text-emerald-400', 'text-red-500');
|
_pysimAdmCanVerify = canVerify;
|
||||||
|
el.classList.remove('text-emerald-600', 'dark:text-emerald-400', 'text-red-500', 'cursor-pointer');
|
||||||
|
el.removeAttribute('onclick');
|
||||||
|
el.removeAttribute('role');
|
||||||
if (verified === null) {
|
if (verified === null) {
|
||||||
el.classList.add('hidden');
|
el.classList.add('hidden');
|
||||||
el.removeAttribute('title');
|
el.removeAttribute('title');
|
||||||
@@ -4954,9 +4967,101 @@ function pysimUpdateAdmIndicator(status) {
|
|||||||
el.classList.remove('hidden');
|
el.classList.remove('hidden');
|
||||||
el.classList.add(verified ? 'text-emerald-600' : 'text-red-500');
|
el.classList.add(verified ? 'text-emerald-600' : 'text-red-500');
|
||||||
if (verified) el.classList.add('dark:text-emerald-400');
|
if (verified) el.classList.add('dark:text-emerald-400');
|
||||||
el.setAttribute('title', t(key
|
let tip;
|
||||||
? (verified ? 'ADM key in the card preset — verified' : 'ADM key in the card preset — not verified')
|
if (key && _pysimAdmBlocked) {
|
||||||
: (verified ? 'Verified — no ADM key in the card preset' : 'No ADM key in the card preset — not verified')));
|
tip = t('ADM is blocked — unblock the card to try again');
|
||||||
|
} else if (key) {
|
||||||
|
tip = t('ADM key in the card preset — click to verify');
|
||||||
|
if (_pysimAdmAttemptsLeft !== null) tip += ' — ' + _pysimAdmAttemptsLeft + ' ' + t('attempt(s) left');
|
||||||
|
} else {
|
||||||
|
tip = t(verified ? 'Verified — no ADM key in the card preset' : 'No ADM key in the card preset — not verified');
|
||||||
|
}
|
||||||
|
el.setAttribute('title', tip);
|
||||||
|
if (canVerify) {
|
||||||
|
el.classList.add('cursor-pointer');
|
||||||
|
el.setAttribute('role', 'button');
|
||||||
|
el.setAttribute('onclick', 'pysimVerifyAdm()');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirmation text before a repeated ADM attempt (null on the first try).
|
||||||
|
// Every wrong key consumes an attempt; a blocked ADM is unrecoverable here.
|
||||||
|
function pysimAdmRetryPrompt(attemptsLeft) {
|
||||||
|
if (attemptsLeft === null || attemptsLeft === undefined) return null;
|
||||||
|
if (attemptsLeft <= 1) return t('This is the last attempt before the ADM is blocked. Try again?');
|
||||||
|
return t('ADM verification already failed') + ' — ' + attemptsLeft + ' ' + t('attempt(s) left')
|
||||||
|
+ '. ' + t('A wrong key can block the ADM permanently. Try again?');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the ADM PIN from the matched card preset (top-bar indicator or the
|
||||||
|
// file manager's security-error hint). `statusEl` receives the result inline
|
||||||
|
// when given; otherwise the card status line is used. Never retries by
|
||||||
|
// itself: every attempt after a failure is confirmed first.
|
||||||
|
async function pysimVerifyAdm(statusEl) {
|
||||||
|
if (_pysimAdmVerifying) return;
|
||||||
|
const preset = cardsMatchedPreset();
|
||||||
|
if (!cardsAdmPresent(preset)) return;
|
||||||
|
const el = statusEl || document.getElementById('pysim-status');
|
||||||
|
if (_pysimAdmBlocked) {
|
||||||
|
if (el) el.textContent = t('ADM is blocked — unblock the card to try again');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const prompt = pysimAdmRetryPrompt(_pysimAdmAttemptsLeft);
|
||||||
|
if (prompt && !confirm(prompt)) return;
|
||||||
|
_pysimAdmVerifying = true;
|
||||||
|
if (el) el.textContent = t('Verifying ADM...');
|
||||||
|
let verifiedOverride;
|
||||||
|
try {
|
||||||
|
const data = await pysimFetch('/api/verify-adm', { adm: preset.adm });
|
||||||
|
if (data.ok) {
|
||||||
|
_pysimAdmAttemptsLeft = null;
|
||||||
|
_pysimAdmBlocked = false;
|
||||||
|
verifiedOverride = true;
|
||||||
|
if (el) el.textContent = t('ADM verified');
|
||||||
|
} else if (data.blocked) {
|
||||||
|
_pysimAdmBlocked = true;
|
||||||
|
if (el) el.textContent = t('ADM is blocked');
|
||||||
|
} else if (typeof data.attempts_left === 'number') {
|
||||||
|
_pysimAdmAttemptsLeft = data.attempts_left;
|
||||||
|
if (el) el.textContent = t('ADM verification failed') + ' — ' + data.attempts_left + ' ' + t('attempt(s) left');
|
||||||
|
} else if (el) {
|
||||||
|
el.textContent = 'SW: ' + (data.sw || '?') + ' — ' + (data.error || 'Error');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (el) el.textContent = t('Error') + ': ' + e.message;
|
||||||
|
} finally {
|
||||||
|
_pysimAdmVerifying = false;
|
||||||
|
const base = _pysimLastStatus || { connected: true };
|
||||||
|
pysimUpdateAdmIndicator(Object.assign({}, base,
|
||||||
|
verifiedOverride === undefined ? {} : { adm_verified: verifiedOverride }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SW codes that mean "verify the ADM PIN first" (access condition / security).
|
||||||
|
function pysimAdmSecuritySw(sw) {
|
||||||
|
return sw === '6982' || sw === '9804';
|
||||||
|
}
|
||||||
|
|
||||||
|
// A new card session (equip/removal) forgets the attempt/blocked state: the
|
||||||
|
// counter belongs to the physical card that was just replaced or reset.
|
||||||
|
function pysimAdmResetAttempts() {
|
||||||
|
_pysimAdmAttemptsLeft = null;
|
||||||
|
_pysimAdmBlocked = false;
|
||||||
|
_pysimAdmStateKey = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render a failed file-manager operation; when the SW is a security error and
|
||||||
|
// the matched preset carries an ADM, offer verification inline (the key is
|
||||||
|
// only ever sent on an explicit click).
|
||||||
|
function pysimFsShowError(statusEl, sw, error) {
|
||||||
|
statusEl.textContent = 'SW: ' + (sw || '?') + ' — ' + (error || 'Error');
|
||||||
|
if (!pysimAdmSecuritySw(sw) || !cardsAdmPresent(cardsMatchedPreset()) || _pysimAdmBlocked) return;
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.textContent = t('Verify ADM');
|
||||||
|
btn.className = 'ml-2 px-2 py-0.5 text-xs rounded bg-amber-600 text-white hover:bg-amber-700';
|
||||||
|
btn.onclick = () => pysimVerifyAdm(statusEl);
|
||||||
|
statusEl.appendChild(document.createTextNode(' '));
|
||||||
|
statusEl.appendChild(btn);
|
||||||
}
|
}
|
||||||
|
|
||||||
// EF.ICCID digits of the equipped card, printed next to the card image
|
// EF.ICCID digits of the equipped card, printed next to the card image
|
||||||
@@ -7082,7 +7187,7 @@ async function pysimFsRead() {
|
|||||||
body.mode = 'raw'; // decoding is client-side (works offline in snapshots too)
|
body.mode = 'raw'; // decoding is client-side (works offline in snapshots too)
|
||||||
const data = await pysimFetch('/api/read', body);
|
const data = await pysimFetch('/api/read', body);
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
statusEl.textContent = 'SW: ' + (data.sw || '?') + ' — ' + (data.error || 'Error');
|
pysimFsShowError(statusEl, data.sw, data.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
statusEl.textContent = 'SW: ' + data.sw + ' OK';
|
statusEl.textContent = 'SW: ' + data.sw + ' OK';
|
||||||
@@ -7209,7 +7314,7 @@ async function pysimFsSave() {
|
|||||||
pysimFsCancel();
|
pysimFsCancel();
|
||||||
pysimFsRead();
|
pysimFsRead();
|
||||||
} else {
|
} else {
|
||||||
statusEl.textContent = 'SW: ' + (data.sw || '?') + ' — ' + (data.error || 'Error');
|
pysimFsShowError(statusEl, data.sw, data.error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Record file
|
// Record file
|
||||||
@@ -7227,7 +7332,7 @@ async function pysimFsSave() {
|
|||||||
body.record_nr = parseInt(num);
|
body.record_nr = parseInt(num);
|
||||||
const data = await pysimFetch('/api/write', body);
|
const data = await pysimFetch('/api/write', body);
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
statusEl.textContent = 'SW: ' + (data.sw || '?') + ' — ' + (data.error || 'Error') + ' (record ' + num + ')';
|
pysimFsShowError(statusEl, data.sw, (data.error || 'Error') + ' (record ' + num + ')');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8054,6 +8159,7 @@ async function pysimResetCardData(refreshStatus) {
|
|||||||
function pysimCardStateUpdate(status) {
|
function pysimCardStateUpdate(status) {
|
||||||
if (!status || typeof status.connected !== 'boolean') return;
|
if (!status || typeof status.connected !== 'boolean') return;
|
||||||
_pysimServerAvailable = true;
|
_pysimServerAvailable = true;
|
||||||
|
_pysimLastStatus = status;
|
||||||
_pysimCardEquipped = !!status.connected;
|
_pysimCardEquipped = !!status.connected;
|
||||||
_pysimEquipping = !!status.equipping;
|
_pysimEquipping = !!status.equipping;
|
||||||
_pysimCardIccid = status.connected ? (status.iccid || null) : null;
|
_pysimCardIccid = status.connected ? (status.iccid || null) : null;
|
||||||
@@ -8071,6 +8177,7 @@ function pysimCardStateUpdate(status) {
|
|||||||
const sessionChanged = _pysimCardSession !== null
|
const sessionChanged = _pysimCardSession !== null
|
||||||
&& status.card_session !== undefined && status.card_session !== _pysimCardSession;
|
&& status.card_session !== undefined && status.card_session !== _pysimCardSession;
|
||||||
if (status.card_session !== undefined) _pysimCardSession = status.card_session;
|
if (status.card_session !== undefined) _pysimCardSession = status.card_session;
|
||||||
|
if (sessionChanged) pysimAdmResetAttempts();
|
||||||
if (status.connected) {
|
if (status.connected) {
|
||||||
pysimSetConnected(true);
|
pysimSetConnected(true);
|
||||||
pysimResetCardData(true);
|
pysimResetCardData(true);
|
||||||
@@ -13313,6 +13420,16 @@ const LANG_RU = {
|
|||||||
'ADM not verified': 'ADM не подтверждён',
|
'ADM not verified': 'ADM не подтверждён',
|
||||||
'ADM key in the card preset — verified': 'Ключ ADM в предустановке — подтверждён',
|
'ADM key in the card preset — verified': 'Ключ ADM в предустановке — подтверждён',
|
||||||
'ADM key in the card preset — not verified': 'Ключ ADM в предустановке — не подтверждён',
|
'ADM key in the card preset — not verified': 'Ключ ADM в предустановке — не подтверждён',
|
||||||
|
'ADM key in the card preset — click to verify': 'Ключ ADM в предустановке — нажмите для проверки',
|
||||||
|
'Verify ADM': 'Проверить ADM',
|
||||||
|
'Verifying ADM...': 'Проверка ADM...',
|
||||||
|
'ADM verification failed': 'Проверка ADM не удалась',
|
||||||
|
'ADM verification already failed': 'Проверка ADM уже завершилась неудачей',
|
||||||
|
'attempt(s) left': 'попыток осталось',
|
||||||
|
'A wrong key can block the ADM permanently. Try again?': 'Неверный ключ может навсегда заблокировать ADM. Попробовать снова?',
|
||||||
|
'This is the last attempt before the ADM is blocked. Try again?': 'Это последняя попытка — неверный ключ заблокирует ADM. Попробовать снова?',
|
||||||
|
'ADM is blocked': 'ADM заблокирован',
|
||||||
|
'ADM is blocked — unblock the card to try again': 'ADM заблокирован — разблокируйте карту, чтобы повторить',
|
||||||
'Verified — no ADM key in the card preset': 'Подтверждён — ключа ADM в предустановке нет',
|
'Verified — no ADM key in the card preset': 'Подтверждён — ключа ADM в предустановке нет',
|
||||||
'No ADM key in the card preset — not verified': 'Ключа ADM в предустановке нет — не подтверждён',
|
'No ADM key in the card preset — not verified': 'Ключа ADM в предустановке нет — не подтверждён',
|
||||||
'SCP80 preset complete': 'Предустановка SCP80 заполнена',
|
'SCP80 preset complete': 'Предустановка SCP80 заполнена',
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simple-v210';
|
const CACHE = 'simple-v211';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
let code = '';
|
||||||
|
for (const fn of ['pysimAdmRetryPrompt', 'pysimAdmSecuritySw']) {
|
||||||
|
code += extractFunc(html, fn) + '\n';
|
||||||
|
}
|
||||||
|
code += 'globalThis.t = s => s;\n';
|
||||||
|
eval(code);
|
||||||
|
|
||||||
|
test('pysimAdmRetryPrompt is silent on the first try', () => {
|
||||||
|
assert.strictEqual(pysimAdmRetryPrompt(null), null);
|
||||||
|
assert.strictEqual(pysimAdmRetryPrompt(undefined), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pysimAdmRetryPrompt warns with the remaining attempts', () => {
|
||||||
|
const msg = pysimAdmRetryPrompt(3);
|
||||||
|
assert.ok(msg.includes('3'), msg);
|
||||||
|
assert.ok(msg.includes('attempt(s) left'), msg);
|
||||||
|
assert.ok(msg.includes('block'), msg);
|
||||||
|
assert.ok(msg.includes('Try again?'), msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pysimAdmRetryPrompt uses the stronger last-attempt text', () => {
|
||||||
|
for (const left of [1, 0]) {
|
||||||
|
const msg = pysimAdmRetryPrompt(left);
|
||||||
|
assert.ok(msg.includes('last attempt'), left + ': ' + msg);
|
||||||
|
assert.ok(msg.includes('Try again?'), msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pysimAdmSecuritySw flags only the access-condition SWs', () => {
|
||||||
|
for (const sw of ['6982', '9804']) assert.ok(pysimAdmSecuritySw(sw), sw);
|
||||||
|
for (const sw of ['9000', '6985', '63C2', '', null, undefined]) {
|
||||||
|
assert.ok(!pysimAdmSecuritySw(sw), String(sw));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('file-manager security failures route through the ADM hint', () => {
|
||||||
|
// no raw SW error rendering is left (all three sites use the hint helper)
|
||||||
|
assert.ok(!/statusEl\.textContent = 'SW: ' \+ \(data\.sw/.test(html), 'raw SW error rendering is gone');
|
||||||
|
assert.strictEqual((html.match(/pysimFsShowError\(statusEl, data\.sw/g) || []).length, 3);
|
||||||
|
assert.ok(html.includes("btn.textContent = t('Verify ADM')"));
|
||||||
|
assert.ok(html.includes("el.setAttribute('onclick', 'pysimVerifyAdm()')"));
|
||||||
|
});
|
||||||
@@ -25,6 +25,9 @@ let code = 'var _pysimCardStateKey = null;\nvar _pysimCardSession = null;\n'
|
|||||||
+ 'var _pysimServerAvailable = null;\nvar _pysimCardEquipped = false;\n'
|
+ 'var _pysimServerAvailable = null;\nvar _pysimCardEquipped = false;\n'
|
||||||
+ 'var _pysimProactiveSeq = null;\nvar _pysimStkSig = null;\nvar _pysimAdmVerified = null;\n'
|
+ 'var _pysimProactiveSeq = null;\nvar _pysimStkSig = null;\nvar _pysimAdmVerified = null;\n'
|
||||||
+ 'var _pysimAdmKey = null;\n'
|
+ 'var _pysimAdmKey = null;\n'
|
||||||
|
+ 'var _pysimAdmCanVerify = false;\nvar _pysimAdmAttemptsLeft = null;\n'
|
||||||
|
+ 'var _pysimAdmBlocked = false;\nvar _pysimAdmVerifying = false;\n'
|
||||||
|
+ 'var _pysimAdmStateKey = null;\nvar _pysimLastStatus = null;\n'
|
||||||
+ 'var _pysimHeaderIccid = undefined;\nvar _pysimHeaderScp80 = undefined;\nvar _pysimHeaderScp81 = undefined;\n'
|
+ 'var _pysimHeaderIccid = undefined;\nvar _pysimHeaderScp80 = undefined;\nvar _pysimHeaderScp81 = undefined;\n'
|
||||||
+ 'var _cardsAutoIccid = null;\nvar _pysimCardIccid = null;\n';
|
+ 'var _cardsAutoIccid = null;\nvar _pysimCardIccid = null;\n';
|
||||||
code += extractFunc(html, 'pysimCardStateUpdate') + '\n';
|
code += extractFunc(html, 'pysimCardStateUpdate') + '\n';
|
||||||
@@ -33,6 +36,7 @@ code += extractFunc(html, 'pysimControlDisabled') + '\n';
|
|||||||
code += extractFunc(html, 'pysimProactiveSeqChanged') + '\n';
|
code += extractFunc(html, 'pysimProactiveSeqChanged') + '\n';
|
||||||
code += extractFunc(html, 'pysimStkStatusChanged') + '\n';
|
code += extractFunc(html, 'pysimStkStatusChanged') + '\n';
|
||||||
code += extractFunc(html, 'pysimUpdateAdmIndicator') + '\n';
|
code += extractFunc(html, 'pysimUpdateAdmIndicator') + '\n';
|
||||||
|
code += extractFunc(html, 'pysimAdmResetAttempts') + '\n';
|
||||||
code += extractFunc(html, 'pysimUpdateIccidIndicator') + '\n';
|
code += extractFunc(html, 'pysimUpdateIccidIndicator') + '\n';
|
||||||
code += extractFunc(html, 'pysimUpdatePresetIndicator') + '\n';
|
code += extractFunc(html, 'pysimUpdatePresetIndicator') + '\n';
|
||||||
code += extractFunc(html, 'pysimUpdatePresetIndicators') + '\n';
|
code += extractFunc(html, 'pysimUpdatePresetIndicators') + '\n';
|
||||||
@@ -72,6 +76,12 @@ function setup() {
|
|||||||
_pysimProactiveSeq = null;
|
_pysimProactiveSeq = null;
|
||||||
_pysimAdmVerified = null;
|
_pysimAdmVerified = null;
|
||||||
_pysimAdmKey = null;
|
_pysimAdmKey = null;
|
||||||
|
_pysimAdmCanVerify = false;
|
||||||
|
_pysimAdmAttemptsLeft = null;
|
||||||
|
_pysimAdmBlocked = false;
|
||||||
|
_pysimAdmVerifying = false;
|
||||||
|
_pysimAdmStateKey = null;
|
||||||
|
_pysimLastStatus = null;
|
||||||
_pysimHeaderIccid = undefined;
|
_pysimHeaderIccid = undefined;
|
||||||
_pysimHeaderScp80 = undefined;
|
_pysimHeaderScp80 = undefined;
|
||||||
_pysimHeaderScp81 = undefined;
|
_pysimHeaderScp81 = undefined;
|
||||||
@@ -272,11 +282,12 @@ test('the ADM badge marks an ADM key in the matching preset', () => {
|
|||||||
globalThis.cardsFindByIccid = () => 0;
|
globalThis.cardsFindByIccid = () => 0;
|
||||||
pysimCardStateUpdate(st({ adm_verified: true }));
|
pysimCardStateUpdate(st({ adm_verified: true }));
|
||||||
assert.strictEqual(adm.textContent, 'ADM ✓ ⚿');
|
assert.strictEqual(adm.textContent, 'ADM ✓ ⚿');
|
||||||
assert.strictEqual(adm.title, 'ADM key in the card preset — verified');
|
assert.strictEqual(adm.title, 'ADM key in the card preset — click to verify');
|
||||||
|
assert.ok(adm.classes.has('cursor-pointer'), 'the badge is clickable with a preset key');
|
||||||
// key present, verification lost (e.g. card reset)
|
// key present, verification lost (e.g. card reset)
|
||||||
pysimCardStateUpdate(st({ adm_verified: false }));
|
pysimCardStateUpdate(st({ adm_verified: false }));
|
||||||
assert.strictEqual(adm.textContent, 'ADM ✗ ⚿');
|
assert.strictEqual(adm.textContent, 'ADM ✗ ⚿');
|
||||||
assert.strictEqual(adm.title, 'ADM key in the card preset — not verified');
|
assert.strictEqual(adm.title, 'ADM key in the card preset — click to verify');
|
||||||
// verified manually, preset has no ADM -> no key glyph
|
// verified manually, preset has no ADM -> no key glyph
|
||||||
globalThis.cards = [];
|
globalThis.cards = [];
|
||||||
pysimCardStateUpdate(st({ adm_verified: true }));
|
pysimCardStateUpdate(st({ adm_verified: true }));
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pysim-simple-server"
|
name = "pysim-simple-server"
|
||||||
version = "2.7.7"
|
version = "2.7.8"
|
||||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||||
|
|||||||
@@ -23,9 +23,10 @@ import gsm0338 # registers 'gsm03.38' codec
|
|||||||
from construct import GreedyBytes
|
from construct import GreedyBytes
|
||||||
from osmocom.construct import GsmOrUcs2Adapter
|
from osmocom.construct import GsmOrUcs2Adapter
|
||||||
from osmocom.tlv import BER_TLV_IE
|
from osmocom.tlv import BER_TLV_IE
|
||||||
|
from osmocom.utils import rpad
|
||||||
|
|
||||||
|
|
||||||
VERSION = '2.7.7'
|
VERSION = '2.7.8'
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -2164,13 +2165,15 @@ def _parse_psk_map(raw):
|
|||||||
|
|
||||||
|
|
||||||
def _redact_psk_fields(body):
|
def _redact_psk_fields(body):
|
||||||
"""Copy of a request body with PSK key material masked (keys must never
|
"""Copy of a request body with key material masked (keys must never
|
||||||
reach the logs; identities stay visible for diagnostics)."""
|
reach the logs; identities stay visible for diagnostics)."""
|
||||||
if not isinstance(body, dict):
|
if not isinstance(body, dict):
|
||||||
return body
|
return body
|
||||||
out = dict(body)
|
out = dict(body)
|
||||||
if out.get('psk_hex'):
|
if out.get('psk_hex'):
|
||||||
out['psk_hex'] = '<redacted>'
|
out['psk_hex'] = '<redacted>'
|
||||||
|
if out.get('adm'):
|
||||||
|
out['adm'] = '<redacted>'
|
||||||
psk_map = out.get('psk_map')
|
psk_map = out.get('psk_map')
|
||||||
if isinstance(psk_map, dict):
|
if isinstance(psk_map, dict):
|
||||||
out['psk_map'] = {k: '<redacted>' for k in psk_map}
|
out['psk_map'] = {k: '<redacted>' for k in psk_map}
|
||||||
@@ -2672,6 +2675,32 @@ def _send_status(scc):
|
|||||||
return scc._tp.send_apdu('%sf2000c%s' % (scc.cat_cla, p3))
|
return scc._tp.send_apdu('%sf2000c%s' % (scc.cat_cla, p3))
|
||||||
|
|
||||||
|
|
||||||
|
def _verify_adm(scc, app, adm_hex):
|
||||||
|
"""Verify the card's ADM PIN (TS 102 221 VERIFY) and report the result.
|
||||||
|
|
||||||
|
``adm_hex`` is the key from the matched card preset (4-16 hex digits);
|
||||||
|
short keys are padded to the 8 CHV bytes with 'f', like pySim's
|
||||||
|
verify_adm. The result is structured so the UI can warn about the
|
||||||
|
remaining attempts: every failed VERIFY consumes one, and a blocked ADM
|
||||||
|
cannot be recovered from here (it needs the unblock key).
|
||||||
|
"""
|
||||||
|
chv = getattr(getattr(app, 'card', None), '_adm_chv_num', 0x0A)
|
||||||
|
fc = rpad(str(adm_hex).lower(), 16)
|
||||||
|
_data, sw = scc.send_apdu(scc.cla_byte + '2000' + ('%02X' % chv) + '08' + fc)
|
||||||
|
sw = str(sw).upper()
|
||||||
|
if sw == '9000':
|
||||||
|
rs = getattr(app, 'rs', None)
|
||||||
|
if rs is not None:
|
||||||
|
rs.adm_verified = True
|
||||||
|
return {'ok': True, 'sw': sw}
|
||||||
|
if re.fullmatch(r'63C[0-9A-F]', sw):
|
||||||
|
return {'ok': False, 'sw': sw, 'attempts_left': int(sw[3], 16)}
|
||||||
|
if sw in ('6983', '9804'):
|
||||||
|
return {'ok': False, 'sw': sw, 'blocked': True}
|
||||||
|
return {'ok': False, 'sw': sw,
|
||||||
|
'error': 'Security status not satisfied' if sw == '6982' else 'Error'}
|
||||||
|
|
||||||
|
|
||||||
def _send_event_download(scc, event_type, event_data=None):
|
def _send_event_download(scc, event_type, event_data=None):
|
||||||
"""Send ENVELOPE(Event Download) for the given event type.
|
"""Send ENVELOPE(Event Download) for the given event type.
|
||||||
Builds: CLA C2 0000 Lc D6 [len] (99 01 [type] 82 02 82 81 [extra])"""
|
Builds: CLA C2 0000 Lc D6 [len] (99 01 [type] 82 02 82 81 [extra])"""
|
||||||
@@ -3592,6 +3621,32 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
sys.stderr.write("APDU: %s → ERROR: %s (%dms)\n" % (apdu_hex, str(e), elapsed))
|
sys.stderr.write("APDU: %s → ERROR: %s (%dms)\n" % (apdu_hex, str(e), elapsed))
|
||||||
self._send_json(err, 500)
|
self._send_json(err, 500)
|
||||||
self._log_resp(err)
|
self._log_resp(err)
|
||||||
|
elif self.path == '/api/verify-adm':
|
||||||
|
scc = self.server.scc
|
||||||
|
app = self.server.app
|
||||||
|
if not scc or not app:
|
||||||
|
self._send_json({'error': _err('reader_not_init', lang)}, 503)
|
||||||
|
self._log_resp({'error': _err('reader_not_init', lang)})
|
||||||
|
return
|
||||||
|
body = self._read_body()
|
||||||
|
self._log_req(_redact_psk_fields(body))
|
||||||
|
adm = re.sub(r'\s', '', str(body.get('adm') or '')).upper()
|
||||||
|
if not re.fullmatch(r'(?:[0-9A-F]{2}){2,8}', adm):
|
||||||
|
resp = {'ok': False, 'error': 'adm must be 4-16 hex digits'}
|
||||||
|
self._send_json(resp, 400)
|
||||||
|
self._log_resp(resp)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
with _CARD_LOCK:
|
||||||
|
resp = _verify_adm(scc, app, adm)
|
||||||
|
sys.stderr.write('VERIFY ADM → SW: %s\n' % resp.get('sw'))
|
||||||
|
self._send_json(resp)
|
||||||
|
self._log_resp(resp)
|
||||||
|
except Exception as e:
|
||||||
|
resp = {'ok': False, 'error': str(e)}
|
||||||
|
sys.stderr.write('VERIFY ADM → ERROR: %s\n' % e)
|
||||||
|
self._send_json(resp, 500)
|
||||||
|
self._log_resp(resp)
|
||||||
elif self.path == '/api/status-poll':
|
elif self.path == '/api/status-poll':
|
||||||
scc = self.server.scc
|
scc = self.server.scc
|
||||||
if not scc:
|
if not scc:
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
"""Tests for the ADM verification helper used by ``POST /api/verify-adm``.
|
||||||
|
|
||||||
|
The helper builds the TS 102 221 VERIFY APDU itself (instead of pySim's
|
||||||
|
``verify_chv``) so the raw SW can be reported back: the UI warns about the
|
||||||
|
remaining attempts after a 63Cx and stops trying once the ADM is blocked.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
from pysim_simple_server import server
|
||||||
|
|
||||||
|
|
||||||
|
class FakeScc:
|
||||||
|
"""Records the VERIFY APDU and returns a canned SW."""
|
||||||
|
|
||||||
|
cla_byte = '00'
|
||||||
|
|
||||||
|
def __init__(self, sw='9000'):
|
||||||
|
self.sw = sw
|
||||||
|
self.apdus = []
|
||||||
|
|
||||||
|
def send_apdu(self, pdu):
|
||||||
|
self.apdus.append(pdu)
|
||||||
|
return '', self.sw
|
||||||
|
|
||||||
|
|
||||||
|
def _app(chv=0x0A):
|
||||||
|
return SimpleNamespace(card=SimpleNamespace(_adm_chv_num=chv),
|
||||||
|
rs=SimpleNamespace(adm_verified=False))
|
||||||
|
|
||||||
|
|
||||||
|
class AdmVerifyTests(unittest.TestCase):
|
||||||
|
def test_success_sets_adm_verified(self):
|
||||||
|
app = _app()
|
||||||
|
scc = FakeScc('9000')
|
||||||
|
res = server._verify_adm(scc, app, '0011')
|
||||||
|
self.assertEqual(res, {'ok': True, 'sw': '9000'})
|
||||||
|
self.assertTrue(app.rs.adm_verified)
|
||||||
|
# short keys are padded to the 8 CHV bytes with 'f' (pySim behaviour)
|
||||||
|
self.assertEqual(scc.apdus, ['0020000A08' + '0011' + 'f' * 12])
|
||||||
|
|
||||||
|
def test_full_length_key_is_not_padded(self):
|
||||||
|
scc = FakeScc('9000')
|
||||||
|
server._verify_adm(scc, _app(), 'DEADBEEFDEADBEEF')
|
||||||
|
self.assertEqual(scc.apdus, ['0020000A08' + 'deadbeefdeadbeef'])
|
||||||
|
|
||||||
|
def test_chv_number_comes_from_the_card_model(self):
|
||||||
|
scc = FakeScc('9000')
|
||||||
|
server._verify_adm(scc, _app(chv=0x0B), '0011')
|
||||||
|
self.assertIn('0020000B', scc.apdus[0])
|
||||||
|
|
||||||
|
def test_63cx_reports_attempts_left(self):
|
||||||
|
app = _app()
|
||||||
|
res = server._verify_adm(FakeScc('63C2'), app, '0011')
|
||||||
|
self.assertEqual(res, {'ok': False, 'sw': '63C2', 'attempts_left': 2})
|
||||||
|
self.assertFalse(app.rs.adm_verified)
|
||||||
|
|
||||||
|
def test_last_attempt_reports_zero(self):
|
||||||
|
res = server._verify_adm(FakeScc('63C0'), _app(), '0011')
|
||||||
|
self.assertEqual(res, {'ok': False, 'sw': '63C0', 'attempts_left': 0})
|
||||||
|
|
||||||
|
def test_blocked_sw_marks_blocked(self):
|
||||||
|
for sw in ('6983', '9804'):
|
||||||
|
res = server._verify_adm(FakeScc(sw), _app(), '0011')
|
||||||
|
self.assertEqual(res, {'ok': False, 'sw': sw, 'blocked': True})
|
||||||
|
|
||||||
|
def test_other_sw_is_a_plain_error(self):
|
||||||
|
self.assertEqual(server._verify_adm(FakeScc('6982'), _app(), '0011'),
|
||||||
|
{'ok': False, 'sw': '6982', 'error': 'Security status not satisfied'})
|
||||||
|
self.assertEqual(server._verify_adm(FakeScc('6A88'), _app(), '0011'),
|
||||||
|
{'ok': False, 'sw': '6A88', 'error': 'Error'})
|
||||||
|
|
||||||
|
def test_redaction_masks_adm(self):
|
||||||
|
out = server._redact_psk_fields({'adm': '0011', 'psk_hex': 'AA', 'other': 'x'})
|
||||||
|
self.assertEqual(out, {'adm': '<redacted>', 'psk_hex': '<redacted>', 'other': 'x'})
|
||||||
|
self.assertEqual(server._redact_psk_fields({'adm': ''}), {'adm': ''})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user