v3.26.0: per-TAR default keysets (the preset/SCP80/RAM rework)
Every preset TAR entry carries `keyset` (int 1-15) - the keyset every
operation addressed to that TAR uses: mandatory for any non-zero MSL
(the counters are keyset-linked), optional for MSL 00 (allowed: some
cards reject the no-security 00/00 KIc/KID form). The store fills it
automatically when the preset has a single keyset, the validation
refuses an entry naming an undefined keyset, and the schema is 6 (a v5
file loads leniently; editing it forces the fix).
The keyset selectors are gone: the SCP80 TAR field became a select of
the preset's entries (applying the TAR's keyset to the form and seeding
the SPI1 from its MSL); the RAM operations use the preset's ISD role
entry (the PWA sends only the preset id, the ISD TAR, SPI and the
counter override - the server resolves the keyset and the keys:
presets.tar_keyset, _preset_tar_keyset, _resolve_operation_keyset,
_op_keyset, used by send-ota's APDU path, ram-install, ram-install-app
and cap-compat); the counter probe and the test runner's scp80 steps
default to their TAR's keyset (an explicit kvn stays an override). A
security TAR without a keyset refuses the operation ("set it in the
Cards tab") - never a silent keyless send - and the start buttons wait
for a usable operation-TAR keyset.
The TAR probe checks the preset's TAR list only (the standard
TAR_PROBE_TARS list and the custom-TAR localStorage are gone; a `tars`
list selects a subset, a TAR outside the preset is refused), each row
using its own MSL and default keyset. The Cards editor's TAR table
gained a Keyset column (the only keyset preselected, an amber marker
when a security MSL lacks one).
Docs: help EN/RU, api.md's tar-probe contract, both AGENTS. Tests: the
store (the field, the auto-fill, the schema, the string forms), the
server resolution (send-ota + the refusal, the probe's per-row keysets
and refusals), the PWA (the TAR select, the RAM ISD resolution, the
keyset-free requests) - several fixtures updated for the new schema.
891 frontend + 885 Python; sw cache simple-v434.
This commit is contained in:
+17
-15
@@ -341,28 +341,30 @@ first attempt instead of hammering the card.
|
|||||||
### `POST /api/tar-probe`
|
### `POST /api/tar-probe`
|
||||||
|
|
||||||
Probe which OTA applications the card has registered: one **secured packet per
|
Probe which OTA applications the card has registered: one **secured packet per
|
||||||
TAR** (the standard allocations of TS 101 220 V18.3.0 Annex D, Table D.1 unless
|
TAR of the named preset** (v3.26.0 — the probe checks the preset's TAR list
|
||||||
a `tars` list is given), each carrying a harmless C-APDU (`SELECT MF` by
|
only; a `tars` list may select a subset, and a TAR outside the preset is
|
||||||
default), with the preset's keyset. Each checked TAR uses **its own MSL** when
|
refused with `400`), each carrying a harmless C-APDU (`SELECT MF` by default).
|
||||||
the preset carries the entry, otherwise the request's `spi1`; a TAR that
|
Each row uses **its own MSL** (the request's `spi1` is only a fallback for a
|
||||||
resolves to neither is refused with a hint **before any packet is sent**. The
|
TAR the preset does not carry) and **its own default keyset** — the keyset
|
||||||
resolved SPI1 must carry a counter check (b5b4 ≠ 00) and ask for a PoR.
|
resolved from that TAR's entry, so the request does not carry KIc/KID/keys and
|
||||||
Every accepted packet consumes a counter, which is persisted into the keyset
|
the probe's keyset selector is gone. A TAR whose MSL requires security
|
||||||
like any other operation, so the probe is refused when the SPI1 has no counter
|
(non-zero) without a default keyset, or one naming an undefined keyset, is
|
||||||
check or the named preset does not define the keyset number. A missing
|
refused with a hint **before any packet is sent**. The resolved SPI1 must
|
||||||
`cntr` is seeded from that keyset (v3.20.0; an explicit value wins but never
|
carry a counter check (b5b4 ≠ 00) and ask for a PoR. Every accepted packet
|
||||||
lowers the stored counter). The delivered SMS carries the OTA CPI IE by
|
consumes a counter, which is persisted into that row's keyset like any other
|
||||||
|
operation (each keyset has its own counter, TS 102 225 A.1); an explicit
|
||||||
|
`cntr` seeds the first row only. The delivered SMS carries the OTA CPI IE by
|
||||||
default (`includeCpi`, as in `/api/send-ota`).
|
default (`includeCpi`, as in `/api/send-ota`).
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"preset_id": "…", "kic": "25", "kid": "25", "kicKey": "…", "kidKey": "…",
|
{"preset_id": "…", "spi1": "16", "apdu": "00A40000023F00",
|
||||||
"cntr": "0000000187", "apdu": "00A40000023F00", "tars": ["000000", "B00000"]}
|
"tars": ["000000", "B00000"]}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response:** `{"success": true, "registered": N, "total": M,
|
**Response:** `{"success": true, "registered": N, "total": M,
|
||||||
"final_cntr": "…", "kvn": 2, "spi1": "16", "spi2": "01",
|
"final_cntr": "…", "kvn": 2, "spi1": "16", "spi2": "01",
|
||||||
"results": [{"tar": "000000", "label": "Issuer Security Domain (compact)",
|
"results": [{"tar": "000000", "label": "ISD", "spi1": "16", "kvn": 2,
|
||||||
"spi1": "16", "verdict": "registered", "sw": "9000",
|
"verdict": "registered", "sw": "9000",
|
||||||
"por_status": "por_ok",
|
"por_status": "por_ok",
|
||||||
"por_sw": "6D00", "por_data": ""}], "steps": [...]}`
|
"por_sw": "6D00", "por_data": ""}], "steps": [...]}`
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+6
-6
@@ -282,7 +282,7 @@
|
|||||||
<p class="mb-3">The <strong>SCP80</strong> top-level tab groups the SCP80-related views. It is switched by three pills: <strong>Secured Packet</strong>, <strong>RAM</strong> and <strong>TAR probe</strong>. Assembles SCP80 secured packets per ETSI TS 102 225. When a card is equipped the server reads its <strong>EF.ICCID</strong> (2FE2); if a card preset carries the same number (digits, or the raw EF hex), it is selected automatically in all three SCP80 views.</p>
|
<p class="mb-3">The <strong>SCP80</strong> top-level tab groups the SCP80-related views. It is switched by three pills: <strong>Secured Packet</strong>, <strong>RAM</strong> and <strong>TAR probe</strong>. Assembles SCP80 secured packets per ETSI TS 102 225. When a card is equipped the server reads its <strong>EF.ICCID</strong> (2FE2); if a card preset carries the same number (digits, or the raw EF hex), it is selected automatically in all three SCP80 views.</p>
|
||||||
|
|
||||||
<h3 id="secured-packet" class="text-lg font-medium mb-2">3.1 Secured Packet</h3>
|
<h3 id="secured-packet" class="text-lg font-medium mb-2">3.1 Secured Packet</h3>
|
||||||
<p class="mb-2">Builds SCP80 secured packets per ETSI TS 102 225. The <strong>Keyset</strong> selector next to the card preset selector picks which keyset of the preset supplies the keys and the counter; the send is refused when the preset does not define that keyset number or when its KIc/KID carry different numbers (TS 102 225 A.2). It is disabled until a card preset is selected. The start button stays disabled until a card preset and one of its keysets are selected. The packet's TAR field suggests the preset's TAR entries (each with its description and MSL); applying a preset sets the operation's TAR and seeds the SPI1 from that TAR's <strong>MSL</strong> (Minimum SPI1, TS 102 226 §8.2.1.3.2.4 — the card checks it before the security processing and answers <code class="font-mono text-sm">0A</code> when the packet's SPI1 is below it). A value below the MSL shows an amber warning under the form; a hand-typed TAR keeps the operator's SPI1. The <strong>Counter</strong> field is an override: it starts empty and shows the preset's next counter as its placeholder — an empty field lets the server seed the packet from the stored counter and persist the value the card accepted; a typed value is used for that send only, is never written to the preset, and a lower one never rewinds the stored counter (only forward moves are persisted).</p>
|
<p class="mb-2">Builds SCP80 secured packets per ETSI TS 102 225. The <strong>TAR</strong> selector lists the selected preset's TAR entries (role label or description, MSL and default keyset); choosing one applies that TAR's <strong>default keyset</strong> to the form (its KIc/KID and keys — v3.26.0: the keyset selector is gone, the TAR drives it) and seeds the SPI1 from its <strong>MSL</strong> (Minimum SPI1, TS 102 226 §8.2.1.3.2.4 — the card checks it before the security processing and answers <code class="font-mono text-sm">0A</code> when the packet's SPI1 is below it). A value below the MSL shows an amber warning under the form. The start button stays disabled until a card preset is selected and the operation TAR's keyset is usable: a TAR whose MSL requires security (any non-zero MSL — the counters are keyset-linked) must have a default keyset in the preset, while an MSL 00 TAR may go keyless or still carry one (some cards reject the no-security <code class="font-mono text-sm">00/00</code> KIc/KID form). The send is refused when the preset does not define the named keyset number or when its KIc/KID carry different numbers (TS 102 225 A.2). The <strong>Counter</strong> field is an override: it starts empty and shows the preset's next counter as its placeholder — an empty field lets the server seed the packet from the stored counter and persist the value the card accepted; a typed value is used for that send only, is never written to the preset, and a lower one never rewinds the stored counter (only forward moves are persisted).</p>
|
||||||
<h4 id="packet-structure" class="font-medium mb-1">Packet structure</h4>
|
<h4 id="packet-structure" class="font-medium mb-1">Packet structure</h4>
|
||||||
<table class="w-full text-sm mb-3 border-collapse">
|
<table class="w-full text-sm mb-3 border-collapse">
|
||||||
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Field</th><th class="text-left py-1 px-2">Size</th><th class="text-left py-1 px-2">Description</th></tr></thead>
|
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Field</th><th class="text-left py-1 px-2">Size</th><th class="text-left py-1 px-2">Description</th></tr></thead>
|
||||||
@@ -314,7 +314,7 @@
|
|||||||
<p class="text-sm mb-3">A “Verify vs pySim” button cross-checks the assembled packet against pySim’s reference <code class="font-mono text-sm">OtaDialectSms.encode_cmd</code>. A “Send to Card” button delivers it via SMS-PP-DOWNLOAD ENVELOPE (when connected to the server). The 2-byte CPL prefix is transmitted whenever the packet is ciphered or carries RC/CC/DS (it is part of their input) and whenever the packet is concatenated; a single unprotected SMS keeps pySim’s CHL-first form (TS 31.115 Table 1 NOTE). Under the packet field the app reports its size and how many SMS it takes; a packet that does not fit one SMS is sent as a <strong>concatenated</strong> download (TS 31.115 §4.3) — the packet is split into SMS user-data parts (first SM 132 octets, following ones 134; the first one additionally carries the concatenation and CPI information elements) and the segments are sent in order. More than 5 segments is refused, that being the practical limit of the card’s concatenation buffer. The returned Proof of Receipt is decoded and shown as a PoR status line (status, TAR, counter, raw PoR); the last command’s status word and response data are filled into the <strong>R-APDU parser</strong> panel, and a successful PoR advances the replay counter (when SPI1 carries a counter check — b5 b4 ≠ 00; without one no counter is advanced or saved) and clears the packet — the server persists the accepted counter into the preset, the form’s counter field stays empty (its placeholder follows the stored value) unless the operator typed an override. A <strong>low-counter</strong> verdict (<code class="font-mono text-sm">cntr_low</code>, or a warning ENVELOPE answer that still carries such a PoR) is <em>not</em> recoverable by retrying: every SCP80/RAM operation stops immediately. The card never reports its counter (the PoR’s CNTR field is a copy of the packet’s), so the notice offers a bounded <strong>Sync counter</strong> probe — a read-only command with increasing counter values (doubling steps, a 32-bit ceiling, at most 40 packets) until the card accepts one, then accepted+1 is stored (a search stopped by a security verdict after a counter rejection saves the value too, with an amber warning to check the security settings) — and a <strong>Go to preset</strong> button for raising the counter by hand; nothing else is written automatically.</p>
|
<p class="text-sm mb-3">A “Verify vs pySim” button cross-checks the assembled packet against pySim’s reference <code class="font-mono text-sm">OtaDialectSms.encode_cmd</code>. A “Send to Card” button delivers it via SMS-PP-DOWNLOAD ENVELOPE (when connected to the server). The 2-byte CPL prefix is transmitted whenever the packet is ciphered or carries RC/CC/DS (it is part of their input) and whenever the packet is concatenated; a single unprotected SMS keeps pySim’s CHL-first form (TS 31.115 Table 1 NOTE). Under the packet field the app reports its size and how many SMS it takes; a packet that does not fit one SMS is sent as a <strong>concatenated</strong> download (TS 31.115 §4.3) — the packet is split into SMS user-data parts (first SM 132 octets, following ones 134; the first one additionally carries the concatenation and CPI information elements) and the segments are sent in order. More than 5 segments is refused, that being the practical limit of the card’s concatenation buffer. The returned Proof of Receipt is decoded and shown as a PoR status line (status, TAR, counter, raw PoR); the last command’s status word and response data are filled into the <strong>R-APDU parser</strong> panel, and a successful PoR advances the replay counter (when SPI1 carries a counter check — b5 b4 ≠ 00; without one no counter is advanced or saved) and clears the packet — the server persists the accepted counter into the preset, the form’s counter field stays empty (its placeholder follows the stored value) unless the operator typed an override. A <strong>low-counter</strong> verdict (<code class="font-mono text-sm">cntr_low</code>, or a warning ENVELOPE answer that still carries such a PoR) is <em>not</em> recoverable by retrying: every SCP80/RAM operation stops immediately. The card never reports its counter (the PoR’s CNTR field is a copy of the packet’s), so the notice offers a bounded <strong>Sync counter</strong> probe — a read-only command with increasing counter values (doubling steps, a 32-bit ceiling, at most 40 packets) until the card accepts one, then accepted+1 is stored (a search stopped by a security verdict after a counter rejection saves the value too, with an amber warning to check the security settings) — and a <strong>Go to preset</strong> button for raising the counter by hand; nothing else is written automatically.</p>
|
||||||
|
|
||||||
<h3 id="ram" class="text-lg font-medium mb-2">3.2 RAM</h3>
|
<h3 id="ram" class="text-lg font-medium mb-2">3.2 RAM</h3>
|
||||||
<p class="mb-2">Delivers Remote Application Management operations as SCP80 secured packets via SMS-PP-DOWNLOAD ENVELOPE. The card must support SCP03 (AES or 3DES). A saved card preset from the <strong>Cards</strong> tab provides the keys, the TAR table and the counter (RAM uses the preset's <strong>ISD TAR</strong>, <code class="font-mono text-sm">000000</code> by default; the SPI1 is derived from that TAR's MSL). The <strong>Keyset</strong> selector next to the preset selector picks the keyset whose keys and counter the operation uses. It is disabled until a card preset is selected. The start button stays disabled until a card preset and one of its keysets are selected. The <strong>Format</strong> selector pins the remote command format: <strong>Auto (detect)</strong> probes the read-only GET STATUS command compact and in both expanded codings and keeps the first that the card executes, <strong>Compact</strong> sends the bare C-APDU, and <strong>Expanded (AA, definite length)</strong> / <strong>Expanded (AE 80, indefinite length)</strong> wrap it in the TS 102 226 §5.2.1 scripting template (both execute on the live ISD; a template whose declared lengths do not match its body draws a Bad format TLV, which the operation shows as the step failure). The listing and registry queries follow that format: compact listings keep the chained GET RESPONSE, while an expanded run sends the same P2=00 queries wrapped and unchained (the reference TCA loader’s form, Le=’00’ returns the listing directly), and a registry query the card refuses in the current format is retried in the other formats — the report names the one that served it. A <strong>next-occurrence page</strong> (<code class="font-mono text-sm">P2=03</code>) that a format refuses is retried in the other formats too — the expanded codings first (the serving one first when it is expanded), the compact command string last, because a compact page attempt can consume the page without delivering it (the card answers <code class="font-mono text-sm">61xx</code> and the chained GET RESPONSE fails; every later format then sees the occurrence already gone) — some cards serve the expanded next page only wrapped in the <code class="font-mono text-sm">AA</code> template — and a listing is marked <code class="font-mono text-sm">(first page only)</code> only when the card promised more (<code class="font-mono text-sm">6310</code>) and no format could fetch the follow-up; in <strong>Auto</strong> the explorer also queries entries the bulk listing left without details individually by AID (a few extra packets). The card-resources block also names the card’s inferred <strong>ETSI release</strong> (<code class="font-mono text-sm">uicc.toolkit</code> version → REL-N, the same mapping as the CAP analysis; shown only when the expanded registry returned the versions). A package whose own listing carries no version derives it from its applet instance when one exists — the package’s module AIDs are the applet instance AIDs, and the row is annotated as coming from the app instance — so a card that refuses every tag-list form still shows package versions wherever an applet instance exists.</p>
|
<p class="mb-2">Delivers Remote Application Management operations as SCP80 secured packets via SMS-PP-DOWNLOAD ENVELOPE. The card must support SCP03 (AES or 3DES). A saved card preset from the <strong>Cards</strong> tab provides the keys, the TAR table and the counter (RAM uses the preset's <strong>ISD TAR</strong>, <code class="font-mono text-sm">000000</code> by default; the SPI1 is derived from that TAR's MSL). v3.26.0: the operation's keyset is the ISD TAR entry's <strong>default keyset</strong> — the keyset selector is gone — and the server resolves it (and its keys) from the preset store. The start button stays disabled until a card preset is selected and the ISD TAR's keyset is usable: a non-zero MSL requires a default keyset, an MSL 00 TAR may go keyless. The <strong>Format</strong> selector pins the remote command format: <strong>Auto (detect)</strong> probes the read-only GET STATUS command compact and in both expanded codings and keeps the first that the card executes, <strong>Compact</strong> sends the bare C-APDU, and <strong>Expanded (AA, definite length)</strong> / <strong>Expanded (AE 80, indefinite length)</strong> wrap it in the TS 102 226 §5.2.1 scripting template (both execute on the live ISD; a template whose declared lengths do not match its body draws a Bad format TLV, which the operation shows as the step failure). The listing and registry queries follow that format: compact listings keep the chained GET RESPONSE, while an expanded run sends the same P2=00 queries wrapped and unchained (the reference TCA loader’s form, Le=’00’ returns the listing directly), and a registry query the card refuses in the current format is retried in the other formats — the report names the one that served it. A <strong>next-occurrence page</strong> (<code class="font-mono text-sm">P2=03</code>) that a format refuses is retried in the other formats too — the expanded codings first (the serving one first when it is expanded), the compact command string last, because a compact page attempt can consume the page without delivering it (the card answers <code class="font-mono text-sm">61xx</code> and the chained GET RESPONSE fails; every later format then sees the occurrence already gone) — some cards serve the expanded next page only wrapped in the <code class="font-mono text-sm">AA</code> template — and a listing is marked <code class="font-mono text-sm">(first page only)</code> only when the card promised more (<code class="font-mono text-sm">6310</code>) and no format could fetch the follow-up; in <strong>Auto</strong> the explorer also queries entries the bulk listing left without details individually by AID (a few extra packets). The card-resources block also names the card’s inferred <strong>ETSI release</strong> (<code class="font-mono text-sm">uicc.toolkit</code> version → REL-N, the same mapping as the CAP analysis; shown only when the expanded registry returned the versions). A package whose own listing carries no version derives it from its applet instance when one exists — the package’s module AIDs are the applet instance AIDs, and the row is annotated as coming from the app instance — so a card that refuses every tag-list form still shows package versions wherever an applet instance exists.</p>
|
||||||
|
|
||||||
<h4 id="ram-operations" class="font-medium mb-1">Operations</h4>
|
<h4 id="ram-operations" class="font-medium mb-1">Operations</h4>
|
||||||
<table class="w-full text-sm mb-3 border-collapse">
|
<table class="w-full text-sm mb-3 border-collapse">
|
||||||
@@ -342,13 +342,13 @@
|
|||||||
</section>
|
</section>
|
||||||
<section class="mb-10">
|
<section class="mb-10">
|
||||||
<h3 id="tar-probe" class="text-lg font-medium mb-2">3.3 TAR probe</h3>
|
<h3 id="tar-probe" class="text-lg font-medium mb-2">3.3 TAR probe</h3>
|
||||||
<p class="mb-2">The third SCP80 pill probes <strong>which OTA applications the card has registered</strong>: it sends <strong>one secured packet per selected TAR</strong> — the standard allocations of TS 101 220 V18.3.0 Annex D, Table D.1, each row carrying the document it comes from — with a harmless C-APDU (default <code class="font-mono text-sm">SELECT MF</code>) using the view's preset and keyset — each checked TAR uses its own <strong>MSL</strong> when the preset carries the entry, otherwise the SPI1 from the shared form; a TAR resolving to neither is refused before anything is sent. The resolved SPI1 must carry a counter check (b5b4 ≠ 00) and request a PoR. The <strong>Execute</strong> button stays disabled until a card preset and one of its keysets are selected.</p>
|
<p class="mb-2">The third SCP80 pill probes <strong>which OTA applications the card has registered</strong>: it sends <strong>one secured packet per selected TAR</strong> with a harmless C-APDU (default <code class="font-mono text-sm">SELECT MF</code>). v3.26.0: the rows are the selected preset's TAR list (no arbitrary TARs), each row uses its own <strong>MSL</strong> and its own <strong>default keyset</strong> (resolved from the preset store — the probe's keyset selector is gone), and a TAR whose MSL requires security without a default keyset is refused before anything is sent. The resolved SPI1 must carry a counter check (b5b4 ≠ 00) and request a PoR. The <strong>Execute</strong> button stays disabled until a card preset is selected.</p>
|
||||||
<p class="mb-2">The list is pre-filled with the standard TARs (all checked; <strong>Select all</strong> / <strong>Select none</strong> switch them quickly) and custom TARs can be added with <strong>Add TAR</strong> (three hex bytes plus an optional description). Custom entries are kept in <code class="font-mono text-sm">localStorage</code> on a best-effort basis — losing them is harmless.</p>
|
<p class="mb-2">The list shows the selected preset's TAR entries (all checked; <strong>Select all</strong> / <strong>Select none</strong> switch them quickly); each row's tooltip names its MSL and default keyset.</p>
|
||||||
<p class="mb-2">Every TAR is classified from the card's answer: <strong>registered</strong> (ENVELOPE 9000 + a PoR carrying the application's own status word and data), <strong>accepted, no answer</strong> (a PoR without an R-APDU), <strong>accepted, no PoR</strong> (the packet was silently dropped) and <strong>refused</strong> (a non-9000 ENVELOPE, typically <code class="font-mono text-sm">6200</code> for a TAR without a registered application). The result table appears in the operation popup; the line under Execute summarises <code class="font-mono text-sm">registered N / M</code>.</p>
|
<p class="mb-2">Every TAR is classified from the card's answer: <strong>registered</strong> (ENVELOPE 9000 + a PoR carrying the application's own status word and data), <strong>accepted, no answer</strong> (a PoR without an R-APDU), <strong>accepted, no PoR</strong> (the packet was silently dropped) and <strong>refused</strong> (a non-9000 ENVELOPE, typically <code class="font-mono text-sm">6200</code> for a TAR without a registered application). The result table appears in the operation popup; the line under Execute summarises <code class="font-mono text-sm">registered N / M</code>.</p>
|
||||||
<p class="mb-2">Each packet the card accepts <strong>consumes a counter</strong>, which is saved into the preset's keyset — the probe is refused when the resolved SPI1 has no counter check or the named preset does not define the keyset number. Observed on a live card: <code class="font-mono text-sm">000000</code>, <code class="font-mono text-sm">B00000</code> and <code class="font-mono text-sm">B00001</code> registered (compact-format applications), the expanded-format TARs, USAT, multiplexing and CASD refused with <code class="font-mono text-sm">6200</code>.</p>
|
<p class="mb-2">Each packet the card accepts <strong>consumes a counter</strong>, which is saved into the preset's keyset — the probe is refused when the resolved SPI1 has no counter check, when the preset lacks the row's default keyset (a non-zero MSL requires one) or the entry names an undefined keyset number. Observed on a live card: <code class="font-mono text-sm">000000</code>, <code class="font-mono text-sm">B00000</code> and <code class="font-mono text-sm">B00001</code> registered (compact-format applications), the expanded-format TARs, USAT, multiplexing and CASD refused with <code class="font-mono text-sm">6200</code>.</p>
|
||||||
|
|
||||||
<h2 id="cards" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">4. Cards tab</h2>
|
<h2 id="cards" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">4. Cards tab</h2>
|
||||||
<p class="mb-2">Stores card presets on the server (<code class="font-mono text-sm">~/.pysim-simple-server/card_presets.json</code>; <code class="font-mono text-sm">--card-presets</code> overrides it) so the Secured Packet view can auto-fill keys and parameters, the SCP81 HTTP OTA listener can look up PSK keys, and the SCP80 counters an operation consumed are persisted with the operation itself — a closed tab or a second browser window can no longer lose an increment. A preset carries <strong>several keysets</strong> — a keyset's number is the <code class="font-mono text-sm">b8..b5</code> nibble of its KIc/KID (TS 102 225 §5.1.2/A.2): KIc and KID of a keyset must carry the same number (01–0F; A.2 rejects a mismatch), and each keyset has its own counter on the card (Annex A.1), so counters are tracked per keyset. In the form the keysets are edited as rows — one per number, with <strong>Add keyset</strong> / <strong>Remove keyset</strong>; each row shows the number derived from its KIc/KID byte, and <code class="font-mono text-sm">00</code> (no security) is not accepted as a keyset. The list is loaded when the PWA connects and can be exported/imported as JSON (the import also takes presets exported by older builds, flat or keyset-shaped). Cards is a top-level tab: it shows the <strong>preset list</strong> by default, and <strong>Add preset</strong> / <strong>Edit</strong> open the <strong>editor</strong> (a separate view) — <strong>Save</strong> stores the preset and returns to the list, <strong>Cancel</strong> discards the edits. When the card is equipped its EF.ICCID is read and the preset with the same ICCID is selected automatically in all three SCP80 views; when the equipped card matches <strong>no</strong> preset — or the selected preset belongs to another card — all three SCP80 views show an amber warning above the preset selector. The list highlights the equipped card's preset. The editor groups the fields into bordered blocks: <strong>PIN/ADM</strong> (the optional card codes — PIN1/PUK1/PIN2/PUK2 as 4–8 decimal digits and the ADM key — stored for later use), <strong>TAR</strong> (the TAR table), <strong>SCP80 (GSM 03.48, ETSI TS 102 225)</strong> (the keysets and their counters) and <strong>SCP81 (HTTP OTA)</strong> (the PSK identity/key pair with the key-selection note). The editor's header row also carries the card's <strong>CAT (USAT) release</strong> — the release guard hides CAT items introduced after it (the Simulator's event parameters); unspecified means Rel-12, and an unset release never filters anything away. The header shows gray <strong>SCP80</strong>/<strong>SCP81</strong> markers and a key glyph on the ADM badge when the preset matching the equipped card's ICCID has those settings filled in.</p>
|
<p class="mb-2">Stores card presets on the server (<code class="font-mono text-sm">~/.pysim-simple-server/card_presets.json</code>; <code class="font-mono text-sm">--card-presets</code> overrides it) so the Secured Packet view can auto-fill keys and parameters, the SCP81 HTTP OTA listener can look up PSK keys, and the SCP80 counters an operation consumed are persisted with the operation itself — a closed tab or a second browser window can no longer lose an increment. A preset carries <strong>several keysets</strong> — a keyset's number is the <code class="font-mono text-sm">b8..b5</code> nibble of its KIc/KID (TS 102 225 §5.1.2/A.2): KIc and KID of a keyset must carry the same number (01–0F; A.2 rejects a mismatch), and each keyset has its own counter on the card (Annex A.1), so counters are tracked per keyset. In the form the keysets are edited as rows — one per number, with <strong>Add keyset</strong> / <strong>Remove keyset</strong>; each row shows the number derived from its KIc/KID byte, and <code class="font-mono text-sm">00</code> (no security) is not accepted as a keyset. The list is loaded when the PWA connects and can be exported/imported as JSON (the import also takes presets exported by older builds, flat or keyset-shaped). Cards is a top-level tab: it shows the <strong>preset list</strong> by default, and <strong>Add preset</strong> / <strong>Edit</strong> open the <strong>editor</strong> (a separate view) — <strong>Save</strong> stores the preset and returns to the list, <strong>Cancel</strong> discards the edits. When the card is equipped its EF.ICCID is read and the preset with the same ICCID is selected automatically in all three SCP80 views; when the equipped card matches <strong>no</strong> preset — or the selected preset belongs to another card — all three SCP80 views show an amber warning above the preset selector. The list highlights the equipped card's preset. The editor groups the fields into bordered blocks: <strong>PIN/ADM</strong> (the optional card codes — PIN1/PUK1/PIN2/PUK2 as 4–8 decimal digits and the ADM key — stored for later use), <strong>TAR</strong> (the TAR table — each entry carries its TAR, its MSL and its <strong>default keyset</strong>: the keyset every operation addressed to that TAR uses; mandatory for a non-zero MSL — the counters are keyset-linked — optional for MSL 00, and a preset with a single keyset fills it automatically), <strong>SCP80 (GSM 03.48, ETSI TS 102 225)</strong> (the keysets and their counters) and <strong>SCP81 (HTTP OTA)</strong> (the PSK identity/key pair with the key-selection note). The editor's header row also carries the card's <strong>CAT (USAT) release</strong> — the release guard hides CAT items introduced after it (the Simulator's event parameters); unspecified means Rel-12, and an unset release never filters anything away. The header shows gray <strong>SCP80</strong>/<strong>SCP81</strong> markers and a key glyph on the ADM badge when the preset matching the equipped card's ICCID has those settings filled in.</p>
|
||||||
<table class="w-full text-sm mb-3 border-collapse">
|
<table class="w-full text-sm mb-3 border-collapse">
|
||||||
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Field</th><th class="text-left py-1 px-2">Description</th></tr></thead>
|
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Field</th><th class="text-left py-1 px-2">Description</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
+255
-284
@@ -528,8 +528,6 @@
|
|||||||
<select id="sp-card-sel" onchange="cardsApply(this.value)" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
|
<select id="sp-card-sel" onchange="cardsApply(this.value)" 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="" data-l10n="— Select card —">— Select card —</option>
|
<option value="" data-l10n="— Select card —">— Select card —</option>
|
||||||
</select>
|
</select>
|
||||||
<label class="block mt-2 mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Keyset">Keyset</label>
|
|
||||||
<select id="sp-keyset-sel" onchange="spKeysetChanged()" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 disabled:opacity-40 disabled:cursor-not-allowed"></select>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="sp-preset-warning" class="hidden mb-3 px-3 py-2 rounded border border-amber-400 dark:border-amber-600 bg-amber-50 dark:bg-amber-900/20 text-sm text-amber-700 dark:text-amber-300"></div>
|
<div id="sp-preset-warning" class="hidden mb-3 px-3 py-2 rounded border border-amber-400 dark:border-amber-600 bg-amber-50 dark:bg-amber-900/20 text-sm text-amber-700 dark:text-amber-300"></div>
|
||||||
|
|
||||||
@@ -650,9 +648,8 @@
|
|||||||
|
|
||||||
<div class="flex gap-2 mb-3">
|
<div class="flex gap-2 mb-3">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">TAR (3 bytes)</label>
|
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="TAR (from the preset)">TAR (from the preset)</label>
|
||||||
<input id="sp-tar" oninput="spInvalidate()" list="sp-tar-list" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="B00001" maxlength="6" value="B00001">
|
<select id="sp-tar" onchange="spTarChanged()" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800"></select>
|
||||||
<datalist id="sp-tar-list"></datalist>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Counter (5 bytes)</label>
|
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Counter (5 bytes)</label>
|
||||||
@@ -698,10 +695,6 @@
|
|||||||
<option value="" data-l10n="— Select card —">— Select card —</option>
|
<option value="" data-l10n="— Select card —">— Select card —</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
|
||||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Keyset">Keyset</label>
|
|
||||||
<select id="ram-keyset-sel" onchange="ramKeysetChanged()" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 disabled:opacity-40 disabled:cursor-not-allowed"></select>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Operation">Operation</label>
|
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Operation">Operation</label>
|
||||||
<select id="ram-op" onchange="ramOpChanged()" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
|
<select id="ram-op" onchange="ramOpChanged()" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
|
||||||
@@ -934,26 +927,17 @@
|
|||||||
<option value="" data-l10n="— Select card —">— Select card —</option>
|
<option value="" data-l10n="— Select card —">— Select card —</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
|
||||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Keyset">Keyset</label>
|
|
||||||
<select id="tar-keyset-sel" onchange="tarKeysetChanged()" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 disabled:opacity-40 disabled:cursor-not-allowed"></select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="tar-preset-warning" class="hidden mb-3 px-3 py-2 rounded border border-amber-400 dark:border-amber-600 bg-amber-50 dark:bg-amber-900/20 text-sm text-amber-700 dark:text-amber-300"></div>
|
<div id="tar-preset-warning" class="hidden mb-3 px-3 py-2 rounded border border-amber-400 dark:border-amber-600 bg-amber-50 dark:bg-amber-900/20 text-sm text-amber-700 dark:text-amber-300"></div>
|
||||||
|
|
||||||
<div class="mb-1 flex items-center justify-between">
|
<div class="mb-1 flex items-center justify-between">
|
||||||
<span class="text-xs text-gray-600 dark:text-slate-400" data-l10n="TARs to probe — TS 101 220 V18.3.0 Annex D, Table D.1">TARs to probe — TS 101 220 V18.3.0 Annex D, Table D.1</span>
|
<span class="text-xs text-gray-600 dark:text-slate-400" data-l10n="TARs to probe (the preset's TAR list)">TARs to probe (the preset's TAR list)</span>
|
||||||
<span class="flex gap-1">
|
<span class="flex gap-1">
|
||||||
<button type="button" onclick="tarProbeSelectAll(true)" class="px-2 py-0.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700" data-l10n="Select all">Select all</button>
|
<button type="button" onclick="tarProbeSelectAll(true)" class="px-2 py-0.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700" data-l10n="Select all">Select all</button>
|
||||||
<button type="button" onclick="tarProbeSelectAll(false)" class="px-2 py-0.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700" data-l10n="Select none">Select none</button>
|
<button type="button" onclick="tarProbeSelectAll(false)" class="px-2 py-0.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700" data-l10n="Select none">Select none</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="tar-list" class="mb-2 border border-gray-200 dark:border-slate-700 rounded p-2 max-h-64 overflow-y-auto"></div>
|
<div id="tar-list" class="mb-2 border border-gray-200 dark:border-slate-700 rounded p-2 max-h-64 overflow-y-auto"></div>
|
||||||
<div class="flex gap-2 items-center mb-3">
|
|
||||||
<input id="tar-custom" class="w-24 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono text-center" maxlength="6" placeholder="AABBCC">
|
|
||||||
<input id="tar-custom-note" class="flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="description (optional)">
|
|
||||||
<button type="button" onclick="tarProbeAddCustom()" class="px-3 py-1.5 text-sm rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 whitespace-nowrap" data-l10n="Add TAR">Add TAR</button>
|
|
||||||
</div>
|
|
||||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Probe APDU (hex)">Probe APDU (hex)</label>
|
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Probe APDU (hex)">Probe APDU (hex)</label>
|
||||||
<input id="tar-apdu" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 font-mono mb-3" value="00A40000023F00">
|
<input id="tar-apdu" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 font-mono mb-3" value="00A40000023F00">
|
||||||
<button data-needs="card" data-needs-keyset="tar" onclick="tarExecute()" class="px-5 py-1.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Execute">Execute</button>
|
<button data-needs="card" data-needs-keyset="tar" onclick="tarExecute()" class="px-5 py-1.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Execute">Execute</button>
|
||||||
@@ -1719,6 +1703,7 @@
|
|||||||
<thead><tr>
|
<thead><tr>
|
||||||
<th class="w-28 text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="TAR">TAR</th>
|
<th class="w-28 text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="TAR">TAR</th>
|
||||||
<th class="w-20 text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="MSL">MSL</th>
|
<th class="w-20 text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="MSL">MSL</th>
|
||||||
|
<th class="w-36 text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="Keyset">Keyset</th>
|
||||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="Description">Description</th>
|
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="Description">Description</th>
|
||||||
<th class="w-12 border-b border-gray-200 dark:border-slate-700"></th>
|
<th class="w-12 border-b border-gray-200 dark:border-slate-700"></th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
@@ -1907,7 +1892,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.25.8';
|
const SIMPLE_VERSION = '3.26.0';
|
||||||
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) =====
|
||||||
@@ -5032,20 +5017,71 @@ function spPresetTar(role) {
|
|||||||
return cardsRoleTar(c, role);
|
return cardsRoleTar(c, role);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The TAR field's suggestions: the preset's TAR table, each entry labelled
|
// The SCP80 TAR selector (v3.26.0): the preset's TAR table - the role entries
|
||||||
// with its description (or its role) and MSL - free text stays possible.
|
// labelled by their role, the free rows by their description - each with its
|
||||||
function spTarListRender(preset) {
|
// MSL and default keyset. A change applies the TAR's keyset to the form and
|
||||||
const list = document.getElementById('sp-tar-list');
|
// seeds the SPI1 from its MSL.
|
||||||
if (!list) return;
|
function spTarOptionsHtml(preset) {
|
||||||
const roleLabel = { isd: 'ISD', uiccRfm: 'UICC RFM', usimRfm: 'ADF RFM' };
|
const roleLabel = { isd: 'ISD', uiccRfm: 'UICC RFM', usimRfm: 'ADF RFM' };
|
||||||
list.innerHTML = cardsTarList(preset).map(e => {
|
const list = cardsTarList(preset);
|
||||||
|
if (!list.length) return '<option value="">' + esc(t('— no TAR in the preset —')) + '</option>';
|
||||||
|
return list.map(e => {
|
||||||
|
const bits = [e.tar || ''];
|
||||||
const label = e.desc || roleLabel[e.role] || '';
|
const label = e.desc || roleLabel[e.role] || '';
|
||||||
const text = label + (e.msl ? (label ? ' ' : '') + '(MSL ' + e.msl + ')' : '');
|
if (label) bits.push(label);
|
||||||
return '<option value="' + esc(e.tar || '') + '"' +
|
if (e.msl) bits.push('MSL ' + e.msl);
|
||||||
(text ? ' label="' + esc(text) + '"' : '') + '></option>';
|
if (e.keyset) bits.push(t('Keyset') + ' ' + e.keyset);
|
||||||
|
return '<option value="' + esc(e.tar || '') + '">' + esc(bits.join(' \u00b7 ')) + '</option>';
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render the SCP80 TAR select from a preset (the view's current one when not
|
||||||
|
// given); the previous selection is kept when the new list carries it.
|
||||||
|
function spTarRender(preset) {
|
||||||
|
const sel = document.getElementById('sp-tar');
|
||||||
|
if (!sel) return;
|
||||||
|
if (preset === undefined) {
|
||||||
|
const idx = spPresetIdx();
|
||||||
|
preset = idx >= 0 ? cards[idx] : null;
|
||||||
|
}
|
||||||
|
const prev = sel.value;
|
||||||
|
sel.disabled = !preset;
|
||||||
|
sel.innerHTML = preset ? spTarOptionsHtml(preset)
|
||||||
|
: '<option value="">' + esc(t('— select a card preset —')) + '</option>';
|
||||||
|
if (prev && Array.prototype.some.call(sel.options, o => o.value === prev)) sel.value = prev;
|
||||||
|
else if (sel.options.length) sel.selectedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed the SPI1 field from a TAR's MSL (the option is added when the selector
|
||||||
|
// does not carry it - the MSL is the card's Minimum SPI1, TS 102 226
|
||||||
|
// 8.2.1.3.2.4).
|
||||||
|
function spSpi1Seed(msl) {
|
||||||
|
const spi1Sel = document.getElementById('sp-spi1');
|
||||||
|
if (!spi1Sel || !msl) return;
|
||||||
|
if (spi1Sel.options && typeof Option === 'function' &&
|
||||||
|
!Array.prototype.some.call(spi1Sel.options, o => o.value === msl)) {
|
||||||
|
spi1Sel.appendChild(new Option(msl + ' \u2014 MSL', msl));
|
||||||
|
}
|
||||||
|
spi1Sel.value = msl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A TAR selection applies its preset entry's default keyset (KIc/KID and the
|
||||||
|
// keys; the counter override is cleared) and seeds the SPI1 from its MSL.
|
||||||
|
function spTarChanged() {
|
||||||
|
const sel = document.getElementById('sp-tar');
|
||||||
|
const idx = spPresetIdx();
|
||||||
|
const preset = idx >= 0 ? cards[idx] : null;
|
||||||
|
const entry = preset ? cardsTarEntry(preset, sel ? sel.value : '') : null;
|
||||||
|
if (entry) {
|
||||||
|
const ks = entry.keyset ? spKeysetFor(preset, entry.keyset) : null;
|
||||||
|
if (ks) spKeysetApply(ks);
|
||||||
|
if (entry.msl) spSpi1Seed(entry.msl);
|
||||||
|
}
|
||||||
|
spInvalidate();
|
||||||
|
updateSp();
|
||||||
|
spKeysetSync();
|
||||||
|
}
|
||||||
|
|
||||||
function packToSp(sourceId) {
|
function packToSp(sourceId) {
|
||||||
const val = document.getElementById(sourceId).value;
|
const val = document.getElementById(sourceId).value;
|
||||||
if (!val) return;
|
if (!val) return;
|
||||||
@@ -5055,7 +5091,10 @@ function packToSp(sourceId) {
|
|||||||
const key = spTarKeyForPack(sourceId);
|
const key = spTarKeyForPack(sourceId);
|
||||||
if (key) {
|
if (key) {
|
||||||
_spTarKey = key;
|
_spTarKey = key;
|
||||||
document.getElementById('sp-tar').value = spPresetTar(key);
|
spTarRender();
|
||||||
|
const sel = document.getElementById('sp-tar');
|
||||||
|
const tar = spPresetTar(key);
|
||||||
|
if (sel && Array.prototype.some.call(sel.options, o => o.value === tar)) sel.value = tar;
|
||||||
}
|
}
|
||||||
_spRamFormat = sourceId === 'chain-ram-preview' ? 'auto' : null;
|
_spRamFormat = sourceId === 'chain-ram-preview' ? 'auto' : null;
|
||||||
}
|
}
|
||||||
@@ -10436,8 +10475,10 @@ function scriptsCapFileChanged() {
|
|||||||
// Collect SP params from the SP form (populated by ramApplyCard -> cardsApply)
|
// Collect SP params from the SP form (populated by ramApplyCard -> cardsApply)
|
||||||
function getRamSpParams(selId) {
|
function getRamSpParams(selId) {
|
||||||
// `preset_id` names the server-side preset the operation belongs to: the
|
// `preset_id` names the server-side preset the operation belongs to: the
|
||||||
// server persists the counter it consumes there (v3.8.0). `selId` picks
|
// server persists the counter it consumes there (v3.8.0) and resolves the
|
||||||
// the view's preset selector (RAM by default, the TAR probe its own).
|
// keyset from the preset's ISD TAR entry (v3.26.0 - the RAM keyset
|
||||||
|
// selector is gone). `selId` picks the view's preset selector (RAM by
|
||||||
|
// default, the TAR probe its own).
|
||||||
const sel = document.getElementById(selId || 'ram-card-sel');
|
const sel = document.getElementById(selId || 'ram-card-sel');
|
||||||
const idx = sel ? parseInt(sel.value, 10) : NaN;
|
const idx = sel ? parseInt(sel.value, 10) : NaN;
|
||||||
const preset = (!isNaN(idx) && cards[idx]) ? cards[idx] : null;
|
const preset = (!isNaN(idx) && cards[idx]) ? cards[idx] : null;
|
||||||
@@ -10445,11 +10486,7 @@ function getRamSpParams(selId) {
|
|||||||
preset_id: (preset && preset.id) || '',
|
preset_id: (preset && preset.id) || '',
|
||||||
spi1: document.getElementById('sp-spi1').value,
|
spi1: document.getElementById('sp-spi1').value,
|
||||||
spi2: document.getElementById('sp-spi2-hex').value,
|
spi2: document.getElementById('sp-spi2-hex').value,
|
||||||
kic: document.getElementById('sp-kic-hex').value,
|
tar: preset ? cardsRoleTar(preset, 'isd') : '',
|
||||||
kid: document.getElementById('sp-kid-hex').value,
|
|
||||||
tar: (document.getElementById('sp-tar').value || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase().padEnd(6, '0').slice(0, 6),
|
|
||||||
kicKey: (document.getElementById('sp-kic-key').value || '').replace(/[^0-9a-fA-F]/g, ''),
|
|
||||||
kidKey: (document.getElementById('sp-kid-key').value || '').replace(/[^0-9a-fA-F]/g, ''),
|
|
||||||
ram_format: (document.getElementById('ram-format') || {}).value || 'auto',
|
ram_format: (document.getElementById('ram-format') || {}).value || 'auto',
|
||||||
};
|
};
|
||||||
// Override only when typed; empty = the server seeds the counter from the
|
// Override only when typed; empty = the server seeds the counter from the
|
||||||
@@ -11928,10 +11965,8 @@ async function ramDeleteFromExplorer(aid, withCascade) {
|
|||||||
const ksErr = ramKeysetGuard();
|
const ksErr = ramKeysetGuard();
|
||||||
if (ksErr) { alert(ksErr); return; }
|
if (ksErr) { alert(ksErr); return; }
|
||||||
const sp = getRamSpParams();
|
const sp = getRamSpParams();
|
||||||
if (!sp.kicKey || !sp.kidKey) {
|
// v3.26.0: the keyset fields no longer travel - the server resolves the
|
||||||
alert(t('Select a card preset with keys first (RAM subtab → Card preset)'));
|
// keyset (and its keys) from the preset's ISD TAR entry
|
||||||
return;
|
|
||||||
}
|
|
||||||
const label = withCascade ? t('Delete') + ' (' + t('cascade') + ')' : t('Delete');
|
const label = withCascade ? t('Delete') + ' (' + t('cascade') + ')' : t('Delete');
|
||||||
if (!confirm(label + ' — ' + t('AID:') + ' ' + aid + '?')) return;
|
if (!confirm(label + ' — ' + t('AID:') + ' ' + aid + '?')) return;
|
||||||
const apdu = ramDeleteApdu(aid, withCascade);
|
const apdu = ramDeleteApdu(aid, withCascade);
|
||||||
@@ -12494,59 +12529,27 @@ async function ramInstallApp(sp, op) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== TAR probe (v3.8.0) =====
|
// ===== TAR probe (v3.8.0) =====
|
||||||
// One secured packet per TAR from the preset's keyset, classified by the
|
// One secured packet per TAR of the selected preset, classified by the card's
|
||||||
// card's ENVELOPE verdict and the application's PoR. The standard TAR list
|
// ENVELOPE verdict and the application's PoR. v3.26.0: the probe checks the
|
||||||
// lives on the server (TS 101 220 V18.3.0 Annex D, Table D.1) and is used when
|
// preset's TAR list only - each row uses its TAR's MSL and default keyset, and
|
||||||
// the list field is empty. Each packet the card accepts consumes a counter,
|
// every packet the card accepts consumes a counter, which the server persists
|
||||||
// which the server persists into the keyset (one per accepted TAR).
|
// into that keyset (one per accepted TAR).
|
||||||
|
|
||||||
// TAR values allocated by ETSI (TS 101 220 V18.3.0 Annex D, Table D.1) with
|
// The probe's rows: the selected preset's TAR list (the role entries labelled
|
||||||
// the document each allocation comes from. Kept in sync with the server's
|
// by their role, the free rows by their description; the tooltip carries the
|
||||||
// TAR_PROBE_TARS by a test; custom TARs live in localStorage (best effort).
|
// MSL and the default keyset). There is no custom-TAR input anymore.
|
||||||
const TAR_PROBE_TARS = [
|
|
||||||
{ tar: '000000', label: 'Issuer Security Domain (compact)', doc: 'TS 102 226' },
|
|
||||||
{ tar: 'B20100', label: 'Issuer Security Domain (expanded)', doc: 'TS 102 226' },
|
|
||||||
{ tar: 'B00000', label: 'UICC shared file system RFM (compact)', doc: 'TS 102 226' },
|
|
||||||
{ tar: 'B00001', label: 'ADF RFM (compact)', doc: 'TS 131 116' },
|
|
||||||
{ tar: 'B00010', label: 'SIM file system RFM (compact)', doc: 'TS 131 116' },
|
|
||||||
{ tar: 'B00120', label: 'UICC shared file system RFM (expanded)', doc: 'TS 102 226' },
|
|
||||||
{ tar: 'B00130', label: 'SIM file system RFM (expanded)', doc: 'TS 131 116' },
|
|
||||||
{ tar: 'B00140', label: 'ADF RFM (expanded)', doc: 'TS 131 116' },
|
|
||||||
{ tar: 'B20000', label: 'USAT interpreter', doc: 'TS 131 114' },
|
|
||||||
{ tar: 'B20200', label: 'Multiplexing application', doc: 'TS 102 225' },
|
|
||||||
{ tar: 'B20201', label: 'Controlling Authority Security Domain', doc: 'GP Amendment A' },
|
|
||||||
{ tar: 'B20202', label: 'OMA BCAST audience measurement', doc: 'OMA' },
|
|
||||||
{ tar: 'B20203', label: 'OMA DM LWM2M application', doc: 'OMA' },
|
|
||||||
{ tar: 'B00200', label: 'RFU (control)', doc: '' },
|
|
||||||
];
|
|
||||||
const TAR_PROBE_STORAGE = 'simple_tar_probe';
|
|
||||||
|
|
||||||
// The custom TARs added by the user (best effort: losing them is harmless).
|
|
||||||
function tarProbeCustomLoad() {
|
|
||||||
try {
|
|
||||||
const d = JSON.parse(localStorage.getItem(TAR_PROBE_STORAGE) || '[]');
|
|
||||||
if (!Array.isArray(d)) return [];
|
|
||||||
return d.filter(e => e && /^[0-9A-F]{6}$/.test(String(e.tar || '').toUpperCase()))
|
|
||||||
.map(e => ({ tar: String(e.tar).toUpperCase(), label: String(e.label || ''), custom: true }));
|
|
||||||
} catch (e) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function tarProbeCustomSave(list) {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(TAR_PROBE_STORAGE, JSON.stringify(
|
|
||||||
(list || []).map(e => ({ tar: e.tar, label: e.label || '' }))));
|
|
||||||
} catch (e) { /* best effort */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
// The standard rows plus the custom ones.
|
|
||||||
function tarProbeAllRows() {
|
function tarProbeAllRows() {
|
||||||
return TAR_PROBE_TARS.concat(tarProbeCustomLoad());
|
const idx = tarPresetIdx();
|
||||||
|
const preset = idx >= 0 ? cards[idx] : null;
|
||||||
|
const roleLabel = { isd: 'ISD', uiccRfm: 'UICC RFM', usimRfm: 'ADF RFM' };
|
||||||
|
return cardsTarList(preset).map(e => ({
|
||||||
|
tar: String(e.tar || '').toUpperCase(),
|
||||||
|
label: e.desc || roleLabel[e.role] || '',
|
||||||
|
doc: e.msl ? 'MSL ' + e.msl + (e.keyset ? ' \u00b7 ' + t('Keyset') + ' ' + e.keyset : '') : '',
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// The checklist rows (pure): a checkbox per TAR, checked by default, the spec
|
// The checklist rows (pure): a checkbox per TAR, checked by default.
|
||||||
// document in the tooltip and a remove button on custom rows.
|
|
||||||
function tarProbeRowsHtml(rows) {
|
function tarProbeRowsHtml(rows) {
|
||||||
return (rows || []).map(r => {
|
return (rows || []).map(r => {
|
||||||
const title = r.doc ? ' title="' + esc(r.doc) + '"' : '';
|
const title = r.doc ? ' title="' + esc(r.doc) + '"' : '';
|
||||||
@@ -12554,9 +12557,6 @@ function tarProbeRowsHtml(rows) {
|
|||||||
'<input type="checkbox" class="rounded border-gray-300 dark:border-slate-600" value="' + esc(r.tar) + '" checked>' +
|
'<input type="checkbox" class="rounded border-gray-300 dark:border-slate-600" value="' + esc(r.tar) + '" checked>' +
|
||||||
'<span class="font-mono text-xs">' + esc(r.tar) + '</span>' +
|
'<span class="font-mono text-xs">' + esc(r.tar) + '</span>' +
|
||||||
'<span class="text-xs text-gray-500 dark:text-slate-400">' + esc(r.label || '') + '</span>' +
|
'<span class="text-xs text-gray-500 dark:text-slate-400">' + esc(r.label || '') + '</span>' +
|
||||||
(r.custom
|
|
||||||
? '<button type="button" onclick="tarProbeRemoveCustom(\'' + esc(r.tar) + '\')" class="ml-auto px-1.5 py-0.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-red-100 dark:hover:bg-red-900/40" title="' + esc(t('Remove TAR')) + '">\u2715</button>'
|
|
||||||
: '') +
|
|
||||||
'</label>';
|
'</label>';
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
@@ -12570,34 +12570,20 @@ function tarProbeChecked() {
|
|||||||
|
|
||||||
function tarProbeRender() {
|
function tarProbeRender() {
|
||||||
const box = document.getElementById('tar-list');
|
const box = document.getElementById('tar-list');
|
||||||
if (box) box.innerHTML = tarProbeRowsHtml(tarProbeAllRows());
|
if (!box) return;
|
||||||
|
const rows = tarProbeAllRows();
|
||||||
|
if (!rows.length) {
|
||||||
|
box.innerHTML = '<span class="text-xs text-gray-400 dark:text-slate-500">' +
|
||||||
|
esc(t('Select a card preset with TARs first')) + '</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
box.innerHTML = tarProbeRowsHtml(rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
function tarProbeSelectAll(on) {
|
function tarProbeSelectAll(on) {
|
||||||
document.querySelectorAll('#tar-list input[type="checkbox"]').forEach(cb => { cb.checked = !!on; });
|
document.querySelectorAll('#tar-list input[type="checkbox"]').forEach(cb => { cb.checked = !!on; });
|
||||||
}
|
}
|
||||||
|
|
||||||
function tarProbeAddCustom() {
|
|
||||||
const el = document.getElementById('tar-custom');
|
|
||||||
const noteEl = document.getElementById('tar-custom-note');
|
|
||||||
const tar = (el && el.value || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
|
|
||||||
if (!/^[0-9A-F]{6}$/.test(tar)) { alert(t('A TAR is three hex bytes, e.g. B00001')); return; }
|
|
||||||
const custom = tarProbeCustomLoad();
|
|
||||||
if (!custom.some(e => e.tar === tar)) {
|
|
||||||
custom.push({ tar: tar, label: (noteEl && noteEl.value || '').trim(), custom: true });
|
|
||||||
tarProbeCustomSave(custom);
|
|
||||||
}
|
|
||||||
if (el) el.value = '';
|
|
||||||
if (noteEl) noteEl.value = '';
|
|
||||||
tarProbeRender();
|
|
||||||
}
|
|
||||||
|
|
||||||
function tarProbeRemoveCustom(tar) {
|
|
||||||
const want = String(tar || '').toUpperCase();
|
|
||||||
tarProbeCustomSave(tarProbeCustomLoad().filter(e => e.tar !== want));
|
|
||||||
tarProbeRender();
|
|
||||||
}
|
|
||||||
|
|
||||||
function tarProbeVerdictLabel(verdict) {
|
function tarProbeVerdictLabel(verdict) {
|
||||||
return { registered: 'registered', no_answer: 'accepted, no answer',
|
return { registered: 'registered', no_answer: 'accepted, no answer',
|
||||||
no_por: 'accepted, no PoR', refused: 'refused' }[verdict] || verdict;
|
no_por: 'accepted, no PoR', refused: 'refused' }[verdict] || verdict;
|
||||||
@@ -12694,46 +12680,29 @@ function tarPresetIdx() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function tarApplyCard(idx) {
|
function tarApplyCard(idx) {
|
||||||
// copy the selected preset into the shared SP form fields (the probe reads
|
// v3.26.0: the probe reads its rows from the selected preset (each TAR
|
||||||
// kic/kid/keys/counter from there, like the RAM view does)
|
// carries its own keyset); the shared SP form fields are only the SPI1
|
||||||
|
// fallback - the ISD stays the form default for the other views.
|
||||||
const i = parseInt(idx, 10);
|
const i = parseInt(idx, 10);
|
||||||
if (!isNaN(i) && cards[i]) _ramCardIdx = i;
|
if (!isNaN(i) && cards[i]) _ramCardIdx = i;
|
||||||
_spTarKey = 'isd'; // the probe's checklist TARs are per-row; the ISD is the form default
|
_spTarKey = 'isd';
|
||||||
cardsApply(idx);
|
cardsApply(idx);
|
||||||
|
tarProbeRender();
|
||||||
spPresetWarningRender();
|
spPresetWarningRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
function tarKeysetChanged() {
|
|
||||||
const sel = document.getElementById('tar-keyset-sel');
|
|
||||||
if (!sel) return;
|
|
||||||
const kvn = parseInt(sel.value, 10);
|
|
||||||
const idx = tarPresetIdx();
|
|
||||||
const ks = (idx >= 0 && !isNaN(kvn)) ? spKeysetFor(cards[idx], kvn) : null;
|
|
||||||
if (ks) spKeysetApply(ks);
|
|
||||||
spKeysetSync();
|
|
||||||
}
|
|
||||||
|
|
||||||
function tarKeysetGuard() {
|
|
||||||
const idx = tarPresetIdx();
|
|
||||||
const kic = (document.getElementById('sp-kic-hex') || {}).value;
|
|
||||||
const kid = (document.getElementById('sp-kid-hex') || {}).value;
|
|
||||||
return spKeysetCheck(idx >= 0 ? cards[idx] : null, kic, kid);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function tarExecute() {
|
async function tarExecute() {
|
||||||
const tars = tarProbeChecked();
|
const tars = tarProbeChecked();
|
||||||
if (!tars.length) { alert(t('Select at least one TAR')); return; }
|
if (!tars.length) { alert(t('Select at least one TAR')); return; }
|
||||||
const cardIdx = parseInt(document.getElementById('tar-card-sel').value, 10);
|
const cardIdx = parseInt(document.getElementById('tar-card-sel').value, 10);
|
||||||
if (!isNaN(cardIdx) && cards[cardIdx]) _ramCardIdx = cardIdx;
|
if (!isNaN(cardIdx) && cards[cardIdx]) _ramCardIdx = cardIdx;
|
||||||
spRefreshFromPreset('tar-card-sel');
|
const idx = tarPresetIdx();
|
||||||
const ksErr = tarKeysetGuard();
|
const preset = idx >= 0 ? cards[idx] : null;
|
||||||
if (ksErr) { alert(ksErr); return; }
|
if (!preset) { alert(t('Select a card preset')); return; }
|
||||||
const sp = getRamSpParams('tar-card-sel');
|
// v3.26.0: the probe checks the preset's TARs and resolves each row's
|
||||||
if (!sp.kicKey || !sp.kidKey) {
|
// keyset server-side; only the preset and the (fallback) SPI1 travel.
|
||||||
alert(t('Select a card preset with keys first (RAM subtab → Card preset)'));
|
const spi1 = (document.getElementById('sp-spi1') || {}).value || '';
|
||||||
return;
|
await tarProbe({ preset_id: preset.id, spi1: spi1 }, tars);
|
||||||
}
|
|
||||||
await tarProbe(sp, tars);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Measured NV footprint of an install operation: the server reads GET DATA
|
// Measured NV footprint of an install operation: the server reads GET DATA
|
||||||
@@ -12838,21 +12807,14 @@ async function ramSyncCounter(presetIdx, view) {
|
|||||||
const preset = (Number.isInteger(i) && i >= 0 && cards[i]) ? cards[i] : null;
|
const preset = (Number.isInteger(i) && i >= 0 && cards[i]) ? cards[i] : null;
|
||||||
const box = ramCntrLowBox(view === 'sp' ? 'sp' : 'ram');
|
const box = ramCntrLowBox(view === 'sp' ? 'sp' : 'ram');
|
||||||
if (!preset) { alert(t('No card preset selected')); return; }
|
if (!preset) { alert(t('No card preset selected')); return; }
|
||||||
// The keyset the failed operation used: the originating view's selector
|
|
||||||
// first, the other view's as the fallback (both hold the keyset number as
|
|
||||||
// their option value).
|
|
||||||
const kvnOf = id => {
|
|
||||||
const sel = document.getElementById(id);
|
|
||||||
const v = sel && sel.value !== '' ? parseInt(sel.value, 10) : NaN;
|
|
||||||
return Number.isInteger(v) && v > 0 ? v : null;
|
|
||||||
};
|
|
||||||
const own = view === 'sp' ? 'sp-keyset-sel' : 'ram-keyset-sel';
|
|
||||||
const other = own === 'ram-keyset-sel' ? 'sp-keyset-sel' : 'ram-keyset-sel';
|
|
||||||
const val = id => (document.getElementById(id) || {}).value || '';
|
const val = id => (document.getElementById(id) || {}).value || '';
|
||||||
const body = { preset_id: preset.id };
|
const body = { preset_id: preset.id };
|
||||||
const kvn = kvnOf(own) || kvnOf(other);
|
// v3.26.0: the keyset follows the operation's TAR - the RAM operations are
|
||||||
if (kvn) body.kvn = kvn;
|
// addressed to the ISD role entry, a plain send to its selected TAR (the
|
||||||
const tar = val('sp-tar').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
|
// server resolves it; an explicit `kvn` override stays possible through
|
||||||
|
// the API).
|
||||||
|
const tar = view === 'ram' ? cardsRoleTar(preset, 'isd')
|
||||||
|
: val('sp-tar').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
|
||||||
if (tar) body.tar = tar;
|
if (tar) body.tar = tar;
|
||||||
if (val('sp-spi1')) body.spi1 = val('sp-spi1');
|
if (val('sp-spi1')) body.spi1 = val('sp-spi1');
|
||||||
// The probe needs a PoR: use the transport the failed operation used - the
|
// The probe needs a PoR: use the transport the failed operation used - the
|
||||||
@@ -13011,10 +12973,8 @@ async function ramExecute() {
|
|||||||
// changed in the Cards tab after the RAM form was filled.
|
// changed in the Cards tab after the RAM form was filled.
|
||||||
spRefreshFromPreset('ram-card-sel');
|
spRefreshFromPreset('ram-card-sel');
|
||||||
const sp = getRamSpParams();
|
const sp = getRamSpParams();
|
||||||
if (!sp.kicKey || !sp.kidKey) {
|
// v3.26.0: the keyset fields no longer travel - the server resolves the
|
||||||
alert(t('Select a card preset with keys first (RAM subtab → Card preset)'));
|
// keyset (and its keys) from the preset's ISD TAR entry
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
if (op === 'explore') await ramExplore(sp);
|
if (op === 'explore') await ramExplore(sp);
|
||||||
else if (op === 'install-cap') await ramInstallCap(sp);
|
else if (op === 'install-cap') await ramInstallCap(sp);
|
||||||
@@ -13543,28 +13503,6 @@ function spKeysetCheck(preset, kicHex, kidHex) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// The <option> list of a keyset selector: the preset's keysets, plus the key
|
|
||||||
// version currently in the form when the preset does not define it.
|
|
||||||
function spKeysetOptionsHtml(preset, kicHex, kidHex) {
|
|
||||||
const kvn = spKeysetKvnOf(kicHex) || spKeysetKvnOf(kidHex) || 0;
|
|
||||||
let html = '';
|
|
||||||
let found = false;
|
|
||||||
spKeysetList(preset).forEach(ks => {
|
|
||||||
const v = spKeysetKvnOf(ks.kic);
|
|
||||||
if (v === null) return;
|
|
||||||
if (v === kvn) found = true;
|
|
||||||
html += '<option value="' + v + '"' + (v === kvn ? ' selected' : '') + '>' +
|
|
||||||
'Keyset ' + v + ' \u2014 ' + esc(ks.kic || '??') + '/' + esc(ks.kid || '??') +
|
|
||||||
' \u2014 ' + esc(t('counter')) + ' ' + esc(ks.cntr || '') + '</option>';
|
|
||||||
});
|
|
||||||
if (kvn && !found) {
|
|
||||||
html += '<option value="' + kvn + '" selected>Keyset ' + kvn + ' \u2014 ' +
|
|
||||||
esc(t('not defined in the preset')) + '</option>';
|
|
||||||
}
|
|
||||||
if (!html) html = '<option value="">' + esc(t('\u2014 no keyset \u2014')) + '</option>';
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Keyset editor (Cards form) =====
|
// ===== Keyset editor (Cards form) =====
|
||||||
let _cardsKeysetCount = 0;
|
let _cardsKeysetCount = 0;
|
||||||
|
|
||||||
@@ -13595,12 +13533,20 @@ function cardsKeysetRowsRender(keysets) {
|
|||||||
function cardsKeysetAdd() {
|
function cardsKeysetAdd() {
|
||||||
cardsKeysetRowsRender(cardsKeysetsFromForm().concat([
|
cardsKeysetRowsRender(cardsKeysetsFromForm().concat([
|
||||||
{ kic: '', kid: '', kicKey: '', kidKey: '', cntr: '' }]));
|
{ kic: '', kid: '', kicKey: '', kidKey: '', cntr: '' }]));
|
||||||
|
cardsTarKeysetRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
function cardsKeysetRemove(i) {
|
function cardsKeysetRemove(i) {
|
||||||
const rows = cardsKeysetsFromForm();
|
const rows = cardsKeysetsFromForm();
|
||||||
rows.splice(i, 1);
|
rows.splice(i, 1);
|
||||||
cardsKeysetRowsRender(rows);
|
cardsKeysetRowsRender(rows);
|
||||||
|
cardsTarKeysetRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the TAR table's keyset selects after the keyset table changed (the
|
||||||
|
// TAR rows' own values are read from the DOM first, so nothing is lost).
|
||||||
|
function cardsTarKeysetRefresh() {
|
||||||
|
cardsTarRowsRender(cardsTarsFromTable());
|
||||||
}
|
}
|
||||||
|
|
||||||
function cardsKeysetKvnUpdate(i) {
|
function cardsKeysetKvnUpdate(i) {
|
||||||
@@ -13614,6 +13560,9 @@ function cardsKeysetKvnUpdate(i) {
|
|||||||
el.classList.toggle('text-red-500', !!bad);
|
el.classList.toggle('text-red-500', !!bad);
|
||||||
el.title = bad ? t('KIc and KID must carry the same keyset number (TS 102 225 A.2)')
|
el.title = bad ? t('KIc and KID must carry the same keyset number (TS 102 225 A.2)')
|
||||||
: t('Keyset number (b8..b5 of KIc/KID)');
|
: t('Keyset number (b8..b5 of KIc/KID)');
|
||||||
|
// The TAR table's keyset selects follow the numbers as they are typed
|
||||||
|
// (v3.26.0) - a re-render keeps every field's value (read from the DOM).
|
||||||
|
cardsTarKeysetRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
// The keyset rows as preset entries (empty rows are dropped).
|
// The keyset rows as preset entries (empty rows are dropped).
|
||||||
@@ -13648,11 +13597,42 @@ const CARDS_TAR_ROLE_DESC = {
|
|||||||
usimRfm: 'ADF FS RFM compact format',
|
usimRfm: 'ADF FS RFM compact format',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The keyset cell (v3.26.0): a select of the form's current keysets, the only
|
||||||
|
// one preselected when the form carries exactly one, and an amber marker when
|
||||||
|
// a TAR whose MSL requires security has none (the server refuses to save such
|
||||||
|
// a TAR; the marker shows it before the save).
|
||||||
|
function cardsTarKeysetCell(e, i) {
|
||||||
|
const keysets = cardsKeysetsFromForm();
|
||||||
|
const kvn = e.keyset ? Number(e.keyset) : null;
|
||||||
|
const msl = String(e.msl || '').toUpperCase();
|
||||||
|
const auto = (!kvn && keysets.length === 1) ? spKeysetKvnOf(keysets[0].kic) : null;
|
||||||
|
const sel = kvn || auto;
|
||||||
|
const warn = !sel && msl && msl !== '00';
|
||||||
|
let html = '<td class="py-1 px-2"><div class="flex items-center gap-1">';
|
||||||
|
html += '<select id="cards-tr-' + i + '-keyset" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800" title="' + esc(t('The keyset packets to this TAR use - a non-zero MSL requires one')) + '">' +
|
||||||
|
'<option value=""' + (sel ? '' : ' selected') + '>' + esc(t('— none —')) + '</option>';
|
||||||
|
keysets.forEach(ks => {
|
||||||
|
const v = spKeysetKvnOf(ks.kic);
|
||||||
|
if (v === null) return;
|
||||||
|
html += '<option value="' + v + '"' + (v === sel ? ' selected' : '') + '>' +
|
||||||
|
esc(t('Keyset') + ' ' + v + ' \u2014 ' + (ks.kic || '??') + '/' + (ks.kid || '??')) + '</option>';
|
||||||
|
});
|
||||||
|
html += '</select>';
|
||||||
|
if (warn) {
|
||||||
|
html += '<span class="text-amber-500" title="' +
|
||||||
|
esc(t('This TAR needs a default keyset (its MSL requires security) - set it in the Cards tab')) +
|
||||||
|
'">\u26a0</span>';
|
||||||
|
}
|
||||||
|
html += '</div></td>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
function cardsTarRowHtml(e, i) {
|
function cardsTarRowHtml(e, i) {
|
||||||
const cls = 'border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono';
|
const cls = 'border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono';
|
||||||
const role = CARDS_TAR_ROLES[i] || '';
|
const role = CARDS_TAR_ROLES[i] || '';
|
||||||
let cells = '<td class="py-1 px-2"><input id="cards-tr-' + i + '-tar" class="w-full ' + cls + '" maxlength="6" placeholder="TAR" value="' + esc(e.tar || '') + '"></td>' +
|
let cells = '<td class="py-1 px-2"><input id="cards-tr-' + i + '-tar" class="w-full ' + cls + '" maxlength="6" placeholder="TAR" value="' + esc(e.tar || '') + '"></td>' +
|
||||||
'<td class="py-1 px-2"><input id="cards-tr-' + i + '-msl" class="w-full ' + cls + '" maxlength="2" placeholder="MSL" value="' + esc(e.msl || '') + '" title="' + esc(t('MSL (Minimum SPI1, one hex byte)')) + '"></td>';
|
'<td class="py-1 px-2"><input id="cards-tr-' + i + '-msl" class="w-full ' + cls + '" maxlength="2" placeholder="MSL" value="' + esc(e.msl || '') + '" title="' + esc(t('MSL (Minimum SPI1, one hex byte)')) + '"></td>' +
|
||||||
|
cardsTarKeysetCell(e, i);
|
||||||
if (role) {
|
if (role) {
|
||||||
cells += '<td class="py-1 px-2 text-xs text-gray-600 dark:text-slate-400">' + esc(CARDS_TAR_ROLE_DESC[role] || '') + '</td>' +
|
cells += '<td class="py-1 px-2 text-xs text-gray-600 dark:text-slate-400">' + esc(CARDS_TAR_ROLE_DESC[role] || '') + '</td>' +
|
||||||
'<td class="py-1 px-2"></td>';
|
'<td class="py-1 px-2"></td>';
|
||||||
@@ -13673,7 +13653,7 @@ function cardsTarRowsRender(entries) {
|
|||||||
const list = entries || [];
|
const list = entries || [];
|
||||||
const roles = CARDS_TAR_ROLES.map(role => {
|
const roles = CARDS_TAR_ROLES.map(role => {
|
||||||
const e = list.find(x => x.role === role) || {};
|
const e = list.find(x => x.role === role) || {};
|
||||||
return { tar: e.tar || '', msl: e.msl || '' };
|
return { tar: e.tar || '', msl: e.msl || '', keyset: e.keyset || null };
|
||||||
});
|
});
|
||||||
const free = list.filter(e => !e.role);
|
const free = list.filter(e => !e.role);
|
||||||
_cardsTarCount = free.length;
|
_cardsTarCount = free.length;
|
||||||
@@ -13682,7 +13662,7 @@ function cardsTarRowsRender(entries) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cardsTarAdd() {
|
function cardsTarAdd() {
|
||||||
cardsTarRowsRender(cardsTarsFromTable().concat([{ tar: '', msl: '', desc: '' }]));
|
cardsTarRowsRender(cardsTarsFromTable().concat([{ tar: '', msl: '', desc: '', keyset: null }]));
|
||||||
}
|
}
|
||||||
|
|
||||||
// `i` is the table row index; the role rows are fixed and never removed.
|
// `i` is the table row index; the role rows are fixed and never removed.
|
||||||
@@ -13703,13 +13683,21 @@ function cardsFreeTarsFromForm() {
|
|||||||
return el ? String(el.value).trim() : '';
|
return el ? String(el.value).trim() : '';
|
||||||
};
|
};
|
||||||
const e = { tar: get('tar').replace(/\s/g, '').toUpperCase(),
|
const e = { tar: get('tar').replace(/\s/g, '').toUpperCase(),
|
||||||
msl: get('msl').replace(/\s/g, '').toUpperCase(), desc: get('desc') };
|
msl: get('msl').replace(/\s/g, '').toUpperCase(), desc: get('desc'),
|
||||||
if (!e.tar && !e.msl && !e.desc) continue;
|
keyset: cardsTarKeysetFromForm(idx) };
|
||||||
|
if (!e.tar && !e.msl && !e.desc && !e.keyset) continue;
|
||||||
out.push(e);
|
out.push(e);
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The keyset select of a TAR row as a number (null for none).
|
||||||
|
function cardsTarKeysetFromForm(idx) {
|
||||||
|
const el = document.getElementById('cards-tr-' + idx + '-keyset');
|
||||||
|
const v = el && el.value ? parseInt(el.value, 10) : NaN;
|
||||||
|
return (Number.isInteger(v) && v > 0) ? v : null;
|
||||||
|
}
|
||||||
|
|
||||||
// The TAR table exactly as displayed: the role rows as typed (an emptied field
|
// The TAR table exactly as displayed: the role rows as typed (an emptied field
|
||||||
// stays empty) plus the free rows - the re-render source for Add/Remove, so an
|
// stays empty) plus the free rows - the re-render source for Add/Remove, so an
|
||||||
// emptied field is not silently refilled with its default.
|
// emptied field is not silently refilled with its default.
|
||||||
@@ -13721,7 +13709,8 @@ function cardsTarsFromTable() {
|
|||||||
};
|
};
|
||||||
return { role: role,
|
return { role: role,
|
||||||
tar: get('tar').replace(/\s/g, '').toUpperCase(),
|
tar: get('tar').replace(/\s/g, '').toUpperCase(),
|
||||||
msl: get('msl').replace(/\s/g, '').toUpperCase(), desc: '' };
|
msl: get('msl').replace(/\s/g, '').toUpperCase(), desc: '',
|
||||||
|
keyset: cardsTarKeysetFromForm(i) };
|
||||||
});
|
});
|
||||||
return out.concat(cardsFreeTarsFromForm());
|
return out.concat(cardsFreeTarsFromForm());
|
||||||
}
|
}
|
||||||
@@ -13732,45 +13721,24 @@ function cardsTarsFromTable() {
|
|||||||
function cardsTarsFromForm() {
|
function cardsTarsFromForm() {
|
||||||
return cardsTarsFromTable().map(e => e.role
|
return cardsTarsFromTable().map(e => e.role
|
||||||
? { role: e.role, tar: e.tar || cardsRoleDefault(e.role),
|
? { role: e.role, tar: e.tar || cardsRoleDefault(e.role),
|
||||||
msl: e.msl || CARDS_MSL_DEFAULT, desc: '' }
|
msl: e.msl || CARDS_MSL_DEFAULT, desc: '', keyset: e.keyset || null }
|
||||||
: e);
|
: e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The role rows back to their pre-settled defaults (TAR + MSL 16) and no free
|
// The role rows back to their pre-settled defaults (TAR + MSL 16, no keyset)
|
||||||
// rows.
|
// and no free rows.
|
||||||
function cardsTarsReset() {
|
function cardsTarsReset() {
|
||||||
cardsTarRowsRender(CARDS_TAR_ROLES.map(role => ({
|
cardsTarRowsRender(CARDS_TAR_ROLES.map(role => ({
|
||||||
role: role, tar: cardsRoleDefault(role), msl: CARDS_MSL_DEFAULT, desc: '' })));
|
role: role, tar: cardsRoleDefault(role), msl: CARDS_MSL_DEFAULT, desc: '',
|
||||||
|
keyset: null })));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Keyset selectors (SCP80 / RAM) =====
|
// ===== Keyset resolution (SCP80 / RAM / TAR probe) =====
|
||||||
// The selector follows the KIc/KID bytes: a keyset of the selected preset that
|
// v3.26.0: the keyset selectors are gone - each TAR carries its default keyset
|
||||||
// matches their keyset number is selected; a number the preset does not define
|
// in the preset, the SCP80 TAR select applies it to the form and the RAM
|
||||||
// shows as "not defined" (and the packet actions refuse it).
|
// operations use the ISD role entry (the server resolves the same rule). Only
|
||||||
|
// the start buttons' availability refresh remains here.
|
||||||
function spKeysetSync() {
|
function spKeysetSync() {
|
||||||
const kic = (document.getElementById('sp-kic-hex') || {}).value;
|
|
||||||
const kid = (document.getElementById('sp-kid-hex') || {}).value;
|
|
||||||
// A keyset only exists inside a card preset: with no preset selected the
|
|
||||||
// selector is disabled and cleared, so a stale "not defined in the preset"
|
|
||||||
// entry can never linger.
|
|
||||||
const sync = (selId, idxFn) => {
|
|
||||||
const sel = document.getElementById(selId);
|
|
||||||
if (!sel) return;
|
|
||||||
const idx = idxFn();
|
|
||||||
const preset = idx >= 0 ? cards[idx] : null;
|
|
||||||
if (!preset) {
|
|
||||||
sel.disabled = true;
|
|
||||||
sel.innerHTML = '<option value="">' + esc(t('\u2014 select a card preset \u2014')) + '</option>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sel.disabled = false;
|
|
||||||
sel.innerHTML = spKeysetOptionsHtml(preset, kic, kid);
|
|
||||||
};
|
|
||||||
sync('sp-keyset-sel', spPresetIdx);
|
|
||||||
sync('ram-keyset-sel', ramPresetIdx);
|
|
||||||
sync('tar-keyset-sel', tarPresetIdx);
|
|
||||||
// The start buttons follow the same state: they wait for a preset and a
|
|
||||||
// keyset defined in it.
|
|
||||||
pysimApplyAvailability();
|
pysimApplyAvailability();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13797,26 +13765,6 @@ function spKeysetApply(ks) {
|
|||||||
updateSp();
|
updateSp();
|
||||||
}
|
}
|
||||||
|
|
||||||
function spKeysetChanged() {
|
|
||||||
const sel = document.getElementById('sp-keyset-sel');
|
|
||||||
if (!sel) return;
|
|
||||||
const kvn = parseInt(sel.value, 10);
|
|
||||||
const idx = spPresetIdx();
|
|
||||||
const ks = (idx >= 0 && !isNaN(kvn)) ? spKeysetFor(cards[idx], kvn) : null;
|
|
||||||
if (ks) spKeysetApply(ks);
|
|
||||||
spKeysetSync();
|
|
||||||
}
|
|
||||||
|
|
||||||
function ramKeysetChanged() {
|
|
||||||
const sel = document.getElementById('ram-keyset-sel');
|
|
||||||
if (!sel) return;
|
|
||||||
const kvn = parseInt(sel.value, 10);
|
|
||||||
const idx = ramPresetIdx();
|
|
||||||
const ks = (idx >= 0 && !isNaN(kvn)) ? spKeysetFor(cards[idx], kvn) : null;
|
|
||||||
if (ks) spKeysetApply(ks);
|
|
||||||
spKeysetSync();
|
|
||||||
}
|
|
||||||
|
|
||||||
// A warning for the SCP80/RAM views when the equipped card and the selected
|
// A warning for the SCP80/RAM views when the equipped card and the selected
|
||||||
// preset disagree (pure). Autoselection keeps them in step, so this covers
|
// preset disagree (pure). Autoselection keeps them in step, so this covers
|
||||||
// what it cannot fix: a card with no preset at all, or a manually selected
|
// what it cannot fix: a card with no preset at all, or a manually selected
|
||||||
@@ -13869,30 +13817,64 @@ function ramPresetIdx() {
|
|||||||
// '' when the SCP80 form may build a packet, else the reason. With a preset
|
// '' when the SCP80 form may build a packet, else the reason. With a preset
|
||||||
// selected the keyset number must be one of its keysets; without one only the
|
// selected the keyset number must be one of its keysets; without one only the
|
||||||
// byte rules apply (hand-entered keys stay possible, a workbench flow).
|
// byte rules apply (hand-entered keys stay possible, a workbench flow).
|
||||||
|
// The click-time guard (v3.26.0): the operation's TAR must have a usable
|
||||||
|
// default keyset in the preset - a TAR whose MSL requires security needs one
|
||||||
|
// (the counters are keyset-linked; MSL 00 may go keyless) - and the form's
|
||||||
|
// KIc/KID must stay consistent with it (the fields remain hand-editable).
|
||||||
|
function spTarKeysetGuard(preset, tar) {
|
||||||
|
if (!preset) return '';
|
||||||
|
const entry = cardsTarEntry(preset, tar);
|
||||||
|
if (!entry) return '';
|
||||||
|
if (entry.keyset) {
|
||||||
|
if (!spKeysetFor(preset, entry.keyset)) {
|
||||||
|
return t('Keyset') + ' ' + entry.keyset + ' ' + t('is not defined in the preset');
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const msl = String(entry.msl || '');
|
||||||
|
if (msl && msl !== '00') {
|
||||||
|
return t('TAR') + ' ' + entry.tar + ': ' +
|
||||||
|
t('needs a default keyset (its MSL requires security) - set it in the Cards tab');
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
function spKeysetGuard() {
|
function spKeysetGuard() {
|
||||||
const idx = spPresetIdx();
|
const idx = spPresetIdx();
|
||||||
|
if (idx < 0) return '';
|
||||||
|
const preset = cards[idx];
|
||||||
|
const sel = document.getElementById('sp-tar');
|
||||||
|
const tarErr = spTarKeysetGuard(preset, sel ? sel.value : '');
|
||||||
|
if (tarErr) return tarErr;
|
||||||
const kic = (document.getElementById('sp-kic-hex') || {}).value;
|
const kic = (document.getElementById('sp-kic-hex') || {}).value;
|
||||||
const kid = (document.getElementById('sp-kid-hex') || {}).value;
|
const kid = (document.getElementById('sp-kid-hex') || {}).value;
|
||||||
return spKeysetCheck(idx >= 0 ? cards[idx] : null, kic, kid);
|
return spKeysetCheck(preset, kic, kid);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ramKeysetGuard() {
|
function ramKeysetGuard() {
|
||||||
const idx = ramPresetIdx();
|
const idx = ramPresetIdx();
|
||||||
|
if (idx < 0) return '';
|
||||||
|
const preset = cards[idx];
|
||||||
|
// The RAM operations are addressed to the ISD TAR (v3.26.0)
|
||||||
|
const tarErr = spTarKeysetGuard(preset, cardsRoleTar(preset, 'isd'));
|
||||||
|
if (tarErr) return tarErr;
|
||||||
const kic = (document.getElementById('sp-kic-hex') || {}).value;
|
const kic = (document.getElementById('sp-kic-hex') || {}).value;
|
||||||
const kid = (document.getElementById('sp-kid-hex') || {}).value;
|
const kid = (document.getElementById('sp-kid-hex') || {}).value;
|
||||||
return spKeysetCheck(idx >= 0 ? cards[idx] : null, kic, kid);
|
return spKeysetCheck(preset, kic, kid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Whether a view's start button may run: its preset must be selected and the
|
// Whether a view's start button may run: its preset must be selected and the
|
||||||
// form's keyset number one of that preset's keysets (the click-time guards keep
|
// operation TAR's keyset usable (the click-time guards keep the byte-level
|
||||||
// the byte-level checks). '00'/'00' (no security) with a preset selected stays
|
// checks). '00'/'00' (no security) with a preset selected stays allowed - it
|
||||||
// allowed - it is a deliberate form choice.
|
// is a deliberate form choice.
|
||||||
function spKeysetStartReady(view) {
|
function spKeysetStartReady(view) {
|
||||||
const idx = view === 'ram' ? ramPresetIdx() : (view === 'tar' ? tarPresetIdx() : spPresetIdx());
|
if (view === 'ram') {
|
||||||
if (idx < 0) return false;
|
if (ramPresetIdx() < 0) return false;
|
||||||
const kic = (document.getElementById('sp-kic-hex') || {}).value;
|
return ramKeysetGuard() === '';
|
||||||
const kid = (document.getElementById('sp-kid-hex') || {}).value;
|
}
|
||||||
return spKeysetCheck(cards[idx], kic, kid) === '';
|
if (view === 'tar') return tarPresetIdx() >= 0;
|
||||||
|
if (spPresetIdx() < 0) return false;
|
||||||
|
return spKeysetGuard() === '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function cardsFormValues() {
|
function cardsFormValues() {
|
||||||
@@ -14257,40 +14239,22 @@ function cardsApplyFields(idx, keepTar, keepCntr) {
|
|||||||
const c = cards[parseInt(idx)];
|
const c = cards[parseInt(idx)];
|
||||||
if (!c) return false;
|
if (!c) return false;
|
||||||
// The TAR of the current operation (ISD for RAM/GP, the RFM TAR for the RFM
|
// The TAR of the current operation (ISD for RAM/GP, the RFM TAR for the RFM
|
||||||
// chains) and its MSL: a preset apply seeds the SPI1 from the MSL - the
|
// chains) and its MSL + default keyset (v3.26.0): a preset apply selects
|
||||||
// card's Minimum SPI1 (TS 102 226 8.2.1.3.2.4). keepTar leaves the packet
|
// the TAR in the selector and applies its keyset to the form, seeding the
|
||||||
// TAR alone (the plain send may target a manually entered TAR): its SPI1 is
|
// SPI1 from its MSL - the card's Minimum SPI1 (TS 102 226 8.2.1.3.2.4).
|
||||||
// the operator's value too.
|
// keepTar leaves the packet TAR (and its keyset) alone: the plain send
|
||||||
|
// keeps the operator's selection.
|
||||||
|
spTarRender(c);
|
||||||
if (!keepTar) {
|
if (!keepTar) {
|
||||||
const roleTar = cardsRoleTar(c, _spTarKey);
|
const roleTar = cardsRoleTar(c, _spTarKey);
|
||||||
document.getElementById('sp-tar').value = roleTar;
|
const sel = document.getElementById('sp-tar');
|
||||||
const msl = cardsTarMsl(c, roleTar) || CARDS_MSL_DEFAULT;
|
if (sel && Array.prototype.some.call(sel.options, o => o.value === roleTar)) {
|
||||||
const spi1Sel = document.getElementById('sp-spi1');
|
sel.value = roleTar;
|
||||||
if (spi1Sel) {
|
|
||||||
if (spi1Sel.options && typeof Option === 'function' &&
|
|
||||||
!Array.prototype.some.call(spi1Sel.options, o => o.value === msl)) {
|
|
||||||
spi1Sel.appendChild(new Option(msl + ' \u2014 MSL', msl));
|
|
||||||
}
|
|
||||||
spi1Sel.value = msl;
|
|
||||||
}
|
}
|
||||||
}
|
const entry = cardsTarEntry(c, sel ? sel.value : roleTar);
|
||||||
// The keyset: keep the keyset number the form carries when this preset
|
spSpi1Seed((entry && entry.msl) || CARDS_MSL_DEFAULT);
|
||||||
// defines it, otherwise start from the preset's first keyset.
|
const ks = (entry && entry.keyset) ? spKeysetFor(c, entry.keyset) : null;
|
||||||
const kicNow = (document.getElementById('sp-kic-hex') || {}).value;
|
if (ks) spKeysetApply(ks);
|
||||||
const kidNow = (document.getElementById('sp-kid-hex') || {}).value;
|
|
||||||
const kvnNow = spKeysetKvnOf(kicNow) || spKeysetKvnOf(kidNow) || 0;
|
|
||||||
const ks = (kvnNow && spKeysetFor(c, kvnNow)) || spKeysetList(c)[0] || null;
|
|
||||||
if (ks) {
|
|
||||||
const kicByte = parseInt(ks.kic, 16);
|
|
||||||
document.getElementById('sp-kic-idx').value = ((kicByte >> 4) & 0x0F).toString(16).toUpperCase();
|
|
||||||
document.getElementById('sp-kic-alg').value = (kicByte & 0x0F).toString(16).padStart(2, '0');
|
|
||||||
updateSpKic();
|
|
||||||
const kidByte = parseInt(ks.kid, 16);
|
|
||||||
document.getElementById('sp-kid-idx').value = ((kidByte >> 4) & 0x0F).toString(16).toUpperCase();
|
|
||||||
document.getElementById('sp-kid-alg').value = (kidByte & 0x0F).toString(16).padStart(2, '0');
|
|
||||||
updateSpKid();
|
|
||||||
document.getElementById('sp-kic-key').value = ks.kicKey || '';
|
|
||||||
document.getElementById('sp-kid-key').value = ks.kidKey || '';
|
|
||||||
}
|
}
|
||||||
// The counter field is an override: applying a preset clears it (empty =
|
// The counter field is an override: applying a preset clears it (empty =
|
||||||
// auto, the server seeds from the store); the pre-send refresh keeps a
|
// auto, the server seeds from the store); the pre-send refresh keeps a
|
||||||
@@ -14301,8 +14265,6 @@ function cardsApplyFields(idx, keepTar, keepCntr) {
|
|||||||
if (cntrEl) cntrEl.value = '';
|
if (cntrEl) cntrEl.value = '';
|
||||||
}
|
}
|
||||||
spCntrPlaceholder();
|
spCntrPlaceholder();
|
||||||
if (!keepTar) document.getElementById('sp-tar').value = cardsRoleTar(c, _spTarKey);
|
|
||||||
spTarListRender(c);
|
|
||||||
spInvalidate();
|
spInvalidate();
|
||||||
updateSp();
|
updateSp();
|
||||||
spKeysetSync();
|
spKeysetSync();
|
||||||
@@ -14317,7 +14279,7 @@ function cardsApply(idx, keepTar, keepCntr) {
|
|||||||
// warning must not outlive the selection it described.
|
// warning must not outlive the selection it described.
|
||||||
spKeysetSync();
|
spKeysetSync();
|
||||||
spPresetWarningRender();
|
spPresetWarningRender();
|
||||||
spTarListRender(null);
|
spTarRender(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_genSpBuild();
|
_genSpBuild();
|
||||||
@@ -23972,6 +23934,15 @@ const LANG_RU = {
|
|||||||
'TARs (MSL)': 'TAR (MSL)',
|
'TARs (MSL)': 'TAR (MSL)',
|
||||||
'Each TAR carries its MSL (Minimum SPI1) — the card checks it before the security processing (TS 102 226 8.2.1.3.2.4); the packet SPI1 is derived from it.': 'У каждого TAR есть MSL (минимальный SPI1) — карта проверяет его до обработки защиты (TS 102 226 8.2.1.3.2.4); из него выводится SPI1 пакета.',
|
'Each TAR carries its MSL (Minimum SPI1) — the card checks it before the security processing (TS 102 226 8.2.1.3.2.4); the packet SPI1 is derived from it.': 'У каждого TAR есть MSL (минимальный SPI1) — карта проверяет его до обработки защиты (TS 102 226 8.2.1.3.2.4); из него выводится SPI1 пакета.',
|
||||||
'More TARs — each with its MSL and an optional description': 'Дополнительные TAR — у каждого свой MSL и необязательное описание',
|
'More TARs — each with its MSL and an optional description': 'Дополнительные TAR — у каждого свой MSL и необязательное описание',
|
||||||
|
'TAR (from the preset)': 'TAR (из пресета)',
|
||||||
|
'TARs to probe (the preset\'s TAR list)': 'TAR для проверки (список TAR пресета)',
|
||||||
|
'— no TAR in the preset —': '— в пресете нет TAR —',
|
||||||
|
'— none —': '— нет —',
|
||||||
|
'Select a card preset': 'Выберите пресет карты',
|
||||||
|
'Select a card preset with TARs first': 'Сначала выберите пресет карты с TAR',
|
||||||
|
'The keyset packets to this TAR use - a non-zero MSL requires one': 'Keyset, который используют пакеты к этому TAR — при MSL не 00 он обязателен',
|
||||||
|
'This TAR needs a default keyset (its MSL requires security) - set it in the Cards tab': 'Этому TAR нужен keyset по умолчанию (его MSL требует защиты) — задайте его на вкладке «Карты»',
|
||||||
|
'needs a default keyset (its MSL requires security) - set it in the Cards tab': 'нужен keyset по умолчанию (его MSL требует защиты) — задайте его на вкладке «Карты»',
|
||||||
'Add TAR': 'Добавить TAR',
|
'Add TAR': 'Добавить TAR',
|
||||||
'Remove TAR': 'Удалить TAR',
|
'Remove TAR': 'Удалить TAR',
|
||||||
'Description (optional)': 'Описание (необязательно)',
|
'Description (optional)': 'Описание (необязательно)',
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simple-v433';
|
const CACHE = 'simple-v434';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -34,12 +34,12 @@ for (const fn of ['cardsTarList', 'cardsRoleDefault', 'cardsRoleTar',
|
|||||||
'spPresetCounterValue', 'spCntrPlaceholder', 'spCntrEffective', 'spCntrAhead',
|
'spPresetCounterValue', 'spCntrPlaceholder', 'spCntrEffective', 'spCntrAhead',
|
||||||
'spCntrAdjust',
|
'spCntrAdjust',
|
||||||
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
|
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
|
||||||
'spKeysetOptionsHtml', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender',
|
'spTarOptionsHtml', 'spTarRender', 'spSpi1Seed', 'spTarKeysetGuard', 'cardsTarKeysetCell', 'cardsTarKeysetFromForm', 'cardsTarKeysetRefresh', 'cardsTarRowsRender', 'cardsTarRowHtml', 'cardsTarsFromTable', 'cardsFreeTarsFromForm', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender',
|
||||||
'cardsKeysetAdd', 'cardsKeysetRemove', 'cardsKeysetKvnUpdate',
|
'cardsKeysetAdd', 'cardsKeysetRemove', 'cardsKeysetKvnUpdate',
|
||||||
'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply',
|
'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply',
|
||||||
'spKeysetChanged', 'ramKeysetChanged', 'ramPresetIdx', 'tarPresetIdx', 'spKeysetGuard',
|
'ramPresetIdx', 'tarPresetIdx', 'spKeysetGuard',
|
||||||
'spTarListRender',
|
|
||||||
'ramKeysetGuard',
|
'ramKeysetGuard',
|
||||||
|
'esc',
|
||||||
]) {
|
]) {
|
||||||
code += extractFunc(html, fn) + '\n';
|
code += extractFunc(html, fn) + '\n';
|
||||||
}
|
}
|
||||||
@@ -61,9 +61,22 @@ globalThis.t = s => s;
|
|||||||
function fakeEnv(selValue, selId, presetCntr) {
|
function fakeEnv(selValue, selId, presetCntr) {
|
||||||
const els = {};
|
const els = {};
|
||||||
for (const id of ['sp-spi1', 'sp-spi2-hex', 'sp-kic-idx', 'sp-kic-alg',
|
for (const id of ['sp-spi1', 'sp-spi2-hex', 'sp-kic-idx', 'sp-kic-alg',
|
||||||
'sp-kid-idx', 'sp-kid-alg', 'sp-tar', 'sp-cntr', 'sp-kic-key', 'sp-kid-key']) {
|
'sp-kid-idx', 'sp-kid-alg', 'sp-cntr', 'sp-kic-key', 'sp-kid-key']) {
|
||||||
els[id] = { value: '' };
|
els[id] = { value: '' };
|
||||||
}
|
}
|
||||||
|
// the TAR is a select (v3.26.0): the stub models the surface the renderer
|
||||||
|
// touches - setting innerHTML parses the option values and selectedIndex
|
||||||
|
// moves the value, like a real <select>
|
||||||
|
els['sp-tar'] = {
|
||||||
|
value: '', disabled: false, options: [], _sel: 0, _html: '',
|
||||||
|
set innerHTML(v) {
|
||||||
|
this._html = v;
|
||||||
|
this.options = Array.from(v.matchAll(/value="([^"]*)"/g), m => ({ value: m[1] }));
|
||||||
|
},
|
||||||
|
get innerHTML() { return this._html; },
|
||||||
|
set selectedIndex(i) { this._sel = i; if (this.options[i]) this.value = this.options[i].value; },
|
||||||
|
get selectedIndex() { return this._sel; },
|
||||||
|
};
|
||||||
// the combined key bytes the server reads (updateSpKic maintains them in
|
// the combined key bytes the server reads (updateSpKic maintains them in
|
||||||
// the real form): the fake preset's keyset resolves to kvn 1
|
// the real form): the fake preset's keyset resolves to kvn 1
|
||||||
els['sp-kic-hex'] = { value: '15' };
|
els['sp-kic-hex'] = { value: '15' };
|
||||||
@@ -114,14 +127,16 @@ test('spRefreshFromPreset for the RAM selector targets the ISD TAR', () => {
|
|||||||
assert.strictEqual(env.els['sp-tar'].value, '000000');
|
assert.strictEqual(env.els['sp-tar'].value, '000000');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the pre-send refresh keeps a hand-edited packet TAR (v3.6.21)', () => {
|
test('the pre-send refresh keeps the selected packet TAR', () => {
|
||||||
|
// v3.26.0: the TAR is a preset selector - the pre-send refresh must keep
|
||||||
|
// the operator's selection (a TAR outside the preset is not offered)
|
||||||
const env = fakeEnv('0', 'sp-card-sel', '0000000020');
|
const env = fakeEnv('0', 'sp-card-sel', '0000000020');
|
||||||
env.els['sp-tar'].value = 'AF4D01'; // e.g. a push/link trigger target
|
env.els['sp-tar'].value = 'B00000'; // the UICC RFM entry of the preset
|
||||||
assert.strictEqual(spRefreshFromPreset('sp-card-sel', true), '0000000020');
|
assert.strictEqual(spRefreshFromPreset('sp-card-sel', true), '0000000020');
|
||||||
assert.strictEqual(env.els['sp-tar'].value, 'AF4D01', 'the typed TAR must survive the send');
|
assert.strictEqual(env.els['sp-tar'].value, 'B00000', 'the selected TAR must survive the send');
|
||||||
assert.strictEqual(env.els['sp-cntr'].value, '', 'the counter field stays an override');
|
assert.strictEqual(env.els['sp-cntr'].value, '', 'the counter field stays an override');
|
||||||
assert.match(env.els['sp-cntr'].placeholder, /0000000020/);
|
assert.match(env.els['sp-cntr'].placeholder, /0000000020/);
|
||||||
// an explicit preset apply (or a pack) still sets the TAR
|
// an explicit preset apply (or a pack) selects the operation's TAR
|
||||||
cardsApplyFields(0);
|
cardsApplyFields(0);
|
||||||
assert.strictEqual(env.els['sp-tar'].value, 'B00000');
|
assert.strictEqual(env.els['sp-tar'].value, 'B00000');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,13 +35,13 @@ for (const fn of ['cardsFormValues', 'cardsClearForm', 'cardsEdit',
|
|||||||
'spCntrAhead', 'spCntrLocalSync', 'ramSaveCntr',
|
'spCntrAhead', 'spCntrLocalSync', 'ramSaveCntr',
|
||||||
'cardsApplyFields', 'cardsApply', 'ramApplyCard',
|
'cardsApplyFields', 'cardsApply', 'ramApplyCard',
|
||||||
'cardsScp80Complete', 'cardsScp81Complete', 'cardsAdmPresent',
|
'cardsScp80Complete', 'cardsScp81Complete', 'cardsAdmPresent',
|
||||||
'spTarKeyForPack', 'spPresetTar', 'spTarListRender', 'packToSp',
|
'spTarKeyForPack', 'spPresetTar', 'packToSp',
|
||||||
'esc', 'escHtml',
|
'esc', 'escHtml',
|
||||||
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
|
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
|
||||||
'spKeysetOptionsHtml', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender',
|
'spTarOptionsHtml', 'spTarRender', 'spSpi1Seed', 'spTarKeysetGuard', 'cardsTarKeysetCell', 'cardsTarKeysetFromForm', 'cardsTarKeysetRefresh', 'cardsTarRowsRender', 'cardsTarRowHtml', 'cardsTarsFromTable', 'cardsFreeTarsFromForm', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender',
|
||||||
'cardsKeysetAdd', 'cardsKeysetRemove', 'cardsKeysetKvnUpdate',
|
'cardsKeysetAdd', 'cardsKeysetRemove', 'cardsKeysetKvnUpdate',
|
||||||
'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply', 'spKeysetChanged',
|
'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply',
|
||||||
'ramKeysetChanged', 'ramPresetIdx', 'tarPresetIdx', 'spKeysetGuard', 'ramKeysetGuard',
|
'ramPresetIdx', 'tarPresetIdx', 'spKeysetGuard', 'ramKeysetGuard',
|
||||||
'cardsTarList', 'cardsRoleDefault', 'cardsRoleTar',
|
'cardsTarList', 'cardsRoleDefault', 'cardsRoleTar',
|
||||||
'cardsTarEntry', 'cardsTarMsl', 'cardsTarText', 'spMslWarningText',
|
'cardsTarEntry', 'cardsTarMsl', 'cardsTarText', 'spMslWarningText',
|
||||||
'cardsTarRowHtml', 'cardsTarRowsRender', 'cardsTarAdd', 'cardsTarRemove',
|
'cardsTarRowHtml', 'cardsTarRowsRender', 'cardsTarAdd', 'cardsTarRemove',
|
||||||
@@ -73,12 +73,11 @@ const CARD_IDS = ['cards-name','cards-iccid','cards-release','cards-pin1','cards
|
|||||||
// the TAR table rows: 0-2 are the fixed role rows, 3+ the free rows
|
// the TAR table rows: 0-2 are the fixed role rows, 3+ the free rows
|
||||||
'cards-tr-0-tar','cards-tr-0-msl','cards-tr-1-tar','cards-tr-1-msl',
|
'cards-tr-0-tar','cards-tr-0-msl','cards-tr-1-tar','cards-tr-1-msl',
|
||||||
'cards-tr-2-tar','cards-tr-2-msl',
|
'cards-tr-2-tar','cards-tr-2-msl',
|
||||||
'cards-tr-3-tar','cards-tr-3-msl','cards-tr-3-desc',
|
'cards-tr-3-tar','cards-tr-3-msl','cards-tr-3-desc','cards-tr-3-keyset',
|
||||||
'cards-tr-4-tar','cards-tr-4-msl','cards-tr-4-desc'];
|
'cards-tr-4-tar','cards-tr-4-msl','cards-tr-4-desc','cards-tr-4-keyset'];
|
||||||
const SP_IDS = ['sp-card-sel','sp-tar','sp-spi1','sp-spi2','sp-spi2-sm','sp-spi2-hex',
|
const SP_IDS = ['sp-card-sel','sp-tar','sp-spi1','sp-spi2','sp-spi2-sm','sp-spi2-hex',
|
||||||
'sp-kic-idx','sp-kic-alg','sp-kic-hex','sp-kid-idx','sp-kid-alg','sp-kid-hex',
|
'sp-kic-idx','sp-kic-alg','sp-kic-hex','sp-kid-idx','sp-kid-alg','sp-kid-hex',
|
||||||
'sp-cntr','sp-kic-key','sp-kid-key','sp-apdu','sp-result','sp-spi1-hex','sp-tar-list',
|
'sp-cntr','sp-kic-key','sp-kid-key','sp-apdu','sp-result','sp-spi1-hex'];
|
||||||
'sp-keyset-sel','ram-keyset-sel'];
|
|
||||||
const CHAIN_IDS = ['chain-sim-preview','chain-usim-preview','chain-ram-preview',
|
const CHAIN_IDS = ['chain-sim-preview','chain-usim-preview','chain-ram-preview',
|
||||||
'ber-result','hota-preview'];
|
'ber-result','hota-preview'];
|
||||||
|
|
||||||
@@ -91,10 +90,10 @@ function ksRow(els, i, ks) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function tarRow(els, i, e) {
|
function tarRow(els, i, e) {
|
||||||
for (const field of ['tar', 'msl', 'desc']) {
|
for (const field of ['tar', 'msl', 'desc', 'keyset']) {
|
||||||
const id = 'cards-tr-' + i + '-' + field;
|
const id = 'cards-tr-' + i + '-' + field;
|
||||||
if (!els[id]) els[id] = fakeEl();
|
if (!els[id]) els[id] = fakeEl();
|
||||||
els[id].value = e[field] || '';
|
els[id].value = (e[field] === undefined || e[field] === null) ? '' : String(e[field]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +107,34 @@ function presetFixture(extra) {
|
|||||||
}, extra || {});
|
}, extra || {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A <select>-like fake (the TAR selector): setting innerHTML parses the option
|
||||||
|
// values (and the selected one) and selectedIndex moves the value, like a real
|
||||||
|
// element. The accessors need defineProperties - Object.assign would flatten
|
||||||
|
// them into data properties.
|
||||||
|
function selectEl() {
|
||||||
|
const el = fakeEl();
|
||||||
|
el.options = [];
|
||||||
|
el._sel = 0;
|
||||||
|
el._html = '';
|
||||||
|
el.disabled = false;
|
||||||
|
Object.defineProperties(el, {
|
||||||
|
innerHTML: {
|
||||||
|
get() { return this._html; },
|
||||||
|
set(v) {
|
||||||
|
this._html = v;
|
||||||
|
this.options = Array.from(v.matchAll(/value="([^"]*)"/g), m => ({ value: m[1] }));
|
||||||
|
const sel = v.match(/<option value="([^"]*)" selected/);
|
||||||
|
if (sel) this.value = sel[1];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selectedIndex: {
|
||||||
|
get() { return this._sel; },
|
||||||
|
set(i) { this._sel = i; if (this.options[i]) this.value = this.options[i].value; },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
function fakeEl() {
|
function fakeEl() {
|
||||||
const classes = new Set();
|
const classes = new Set();
|
||||||
return {
|
return {
|
||||||
@@ -134,6 +161,7 @@ let apiCalls = [];
|
|||||||
function setup() {
|
function setup() {
|
||||||
const els = {};
|
const els = {};
|
||||||
for (const id of CARD_IDS.concat(SP_IDS, CHAIN_IDS)) els[id] = fakeEl();
|
for (const id of CARD_IDS.concat(SP_IDS, CHAIN_IDS)) els[id] = fakeEl();
|
||||||
|
els['sp-tar'] = selectEl(); // the TAR selector (v3.26.0)
|
||||||
globalThis.document = { getElementById: id => els[id] || null, querySelectorAll: () => [] };
|
globalThis.document = { getElementById: id => els[id] || null, querySelectorAll: () => [] };
|
||||||
globalThis.cards = [];
|
globalThis.cards = [];
|
||||||
globalThis._cardsEditIdx = null;
|
globalThis._cardsEditIdx = null;
|
||||||
@@ -257,9 +285,9 @@ test('clearing the card form keeps the role TAR/MSL defaults', () => {
|
|||||||
assert.match(els['cards-tars'].innerHTML, /cards-tr-2-tar"[^>]*value="B00001"/);
|
assert.match(els['cards-tars'].innerHTML, /cards-tr-2-tar"[^>]*value="B00001"/);
|
||||||
// ... and the table reads back as the three role entries
|
// ... and the table reads back as the three role entries
|
||||||
assert.deepStrictEqual(cardsTarsFromForm(), [
|
assert.deepStrictEqual(cardsTarsFromForm(), [
|
||||||
{ role: 'isd', tar: '000000', msl: '16', desc: '' },
|
{ role: 'isd', tar: '000000', msl: '16', desc: '', keyset: null },
|
||||||
{ role: 'uiccRfm', tar: 'B00000', msl: '16', desc: '' },
|
{ role: 'uiccRfm', tar: 'B00000', msl: '16', desc: '', keyset: null },
|
||||||
{ role: 'usimRfm', tar: 'B00001', msl: '16', desc: '' },
|
{ role: 'usimRfm', tar: 'B00001', msl: '16', desc: '', keyset: null },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -312,10 +340,10 @@ test('the form values carry the codes and the TAR table', () => {
|
|||||||
_cardsTarCount = 1;
|
_cardsTarCount = 1;
|
||||||
const v = cardsFormValues();
|
const v = cardsFormValues();
|
||||||
assert.deepStrictEqual(v.tars, [
|
assert.deepStrictEqual(v.tars, [
|
||||||
{ role: 'isd', tar: '000001', msl: '0A', desc: '' },
|
{ role: 'isd', tar: '000001', msl: '0A', desc: '', keyset: null },
|
||||||
{ role: 'uiccRfm', tar: 'B00000', msl: '16', desc: '' },
|
{ role: 'uiccRfm', tar: 'B00000', msl: '16', desc: '', keyset: null },
|
||||||
{ role: 'usimRfm', tar: 'B00003', msl: '1A', desc: '' },
|
{ role: 'usimRfm', tar: 'B00003', msl: '1A', desc: '', keyset: null },
|
||||||
{ tar: 'AF4D02', msl: '0A', desc: 'My applet' },
|
{ tar: 'AF4D02', msl: '0A', desc: 'My applet', keyset: null },
|
||||||
]);
|
]);
|
||||||
assert.strictEqual(v.pin1, '1234');
|
assert.strictEqual(v.pin1, '1234');
|
||||||
assert.strictEqual(v.puk1, '12345678');
|
assert.strictEqual(v.puk1, '12345678');
|
||||||
@@ -342,6 +370,11 @@ test('saving a preset posts the form with the pre-settled TAR table', async () =
|
|||||||
els['cards-name'].value = 'New card';
|
els['cards-name'].value = 'New card';
|
||||||
ksRow(els, 0, { kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '0000000001' });
|
ksRow(els, 0, { kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '0000000001' });
|
||||||
globalThis._cardsKeysetCount = 1;
|
globalThis._cardsKeysetCount = 1;
|
||||||
|
// the editor's render preselects the only keyset on every TAR row
|
||||||
|
// (v3.26.0); the fake DOM is static, so the test does it explicitly
|
||||||
|
tarRow(els, 0, { keyset: 1 });
|
||||||
|
tarRow(els, 1, { keyset: 1 });
|
||||||
|
tarRow(els, 2, { keyset: 1 });
|
||||||
// the role fields are pre-settled by the markup; the test's fake elements
|
// the role fields are pre-settled by the markup; the test's fake elements
|
||||||
// start empty - the defaults fill in
|
// start empty - the defaults fill in
|
||||||
els['cards-adm'].value = 'aa bb';
|
els['cards-adm'].value = 'aa bb';
|
||||||
@@ -352,9 +385,9 @@ test('saving a preset posts the form with the pre-settled TAR table', async () =
|
|||||||
assert.deepStrictEqual(apiCalls[0].body.keysets, [
|
assert.deepStrictEqual(apiCalls[0].body.keysets, [
|
||||||
{ kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '0000000001' }]);
|
{ kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '0000000001' }]);
|
||||||
assert.deepStrictEqual(apiCalls[0].body.tars, [
|
assert.deepStrictEqual(apiCalls[0].body.tars, [
|
||||||
{ role: 'isd', tar: '000000', msl: '16', desc: '' },
|
{ role: 'isd', tar: '000000', msl: '16', desc: '', keyset: 1 },
|
||||||
{ role: 'uiccRfm', tar: 'B00000', msl: '16', desc: '' },
|
{ role: 'uiccRfm', tar: 'B00000', msl: '16', desc: '', keyset: 1 },
|
||||||
{ role: 'usimRfm', tar: 'B00001', msl: '16', desc: '' },
|
{ role: 'usimRfm', tar: 'B00001', msl: '16', desc: '', keyset: 1 },
|
||||||
]);
|
]);
|
||||||
assert.strictEqual(apiCalls[0].body.adm, 'AABB');
|
assert.strictEqual(apiCalls[0].body.adm, 'AABB');
|
||||||
});
|
});
|
||||||
@@ -457,11 +490,14 @@ test('applying a preset uses the TAR of the current operation and seeds the SPI1
|
|||||||
cardsApply(0);
|
cardsApply(0);
|
||||||
assert.strictEqual(els['sp-tar'].value, 'B0000D');
|
assert.strictEqual(els['sp-tar'].value, 'B0000D');
|
||||||
assert.strictEqual(els['sp-spi1'].value, '1A');
|
assert.strictEqual(els['sp-spi1'].value, '1A');
|
||||||
// an empty stored TAR -> spec default
|
// a preset without TAR entries (hand-made; the store fills the three
|
||||||
|
// roles): the selector has no options and the SPI1 falls back to the
|
||||||
|
// default MSL
|
||||||
globalThis.cards = [{ name: 'D', keysets: [], tars: [] }];
|
globalThis.cards = [{ name: 'D', keysets: [], tars: [] }];
|
||||||
globalThis._spTarKey = 'isd';
|
globalThis._spTarKey = 'isd';
|
||||||
cardsApply(0);
|
cardsApply(0);
|
||||||
assert.strictEqual(els['sp-tar'].value, '000000');
|
assert.strictEqual(els['sp-tar'].value, '');
|
||||||
|
assert.match(els['sp-tar'].innerHTML, /no TAR in the preset/);
|
||||||
assert.strictEqual(els['sp-spi1'].value, '16');
|
assert.strictEqual(els['sp-spi1'].value, '16');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -494,15 +530,18 @@ test('spPresetTar reads the selected preset and falls back to the spec defaults'
|
|||||||
assert.strictEqual(spPresetTar('uiccRfm'), 'B00000');
|
assert.strictEqual(spPresetTar('uiccRfm'), 'B00000');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the TAR field suggestions come from the preset table', () => {
|
test('the TAR selector lists the preset table', () => {
|
||||||
|
// v3.26.0: the TAR is a select of the preset's entries (role label or
|
||||||
|
// description, MSL and default keyset in the option text)
|
||||||
const els = setup();
|
const els = setup();
|
||||||
spTarListRender(presetFixture({ tars: [
|
spTarRender(presetFixture({ tars: [
|
||||||
{ role: 'isd', tar: '000000', msl: '16' },
|
{ role: 'isd', tar: '000000', msl: '16', keyset: 1 },
|
||||||
{ tar: 'AF4D02', msl: '0A', desc: 'My applet' }] }));
|
{ tar: 'AF4D02', msl: '0A', desc: 'My applet' }] }));
|
||||||
assert.match(els['sp-tar-list'].innerHTML, /value="000000"[^>]*label="ISD \(MSL 16\)"/);
|
assert.match(els['sp-tar'].innerHTML, /value="000000"[^>]*>000000 \u00b7 ISD \u00b7 MSL 16 \u00b7 Keyset 1</);
|
||||||
assert.match(els['sp-tar-list'].innerHTML, /value="AF4D02"[^>]*label="My applet \(MSL 0A\)"/);
|
assert.match(els['sp-tar'].innerHTML, /value="AF4D02"[^>]*>AF4D02 \u00b7 My applet \u00b7 MSL 0A</);
|
||||||
spTarListRender(null);
|
spTarRender(null);
|
||||||
assert.strictEqual(els['sp-tar-list'].innerHTML, '');
|
assert.match(els['sp-tar'].innerHTML, /select a card preset/);
|
||||||
|
assert.strictEqual(els['sp-tar'].disabled, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('packing a chain selects the TAR of its target', () => {
|
test('packing a chain selects the TAR of its target', () => {
|
||||||
@@ -533,17 +572,18 @@ test('packing a chain selects the TAR of its target', () => {
|
|||||||
assert.strictEqual(els['sp-apdu'].value, 'AA0100');
|
assert.strictEqual(els['sp-apdu'].value, 'AA0100');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('packing without a selected preset uses the spec default TAR', () => {
|
test('packing without a selected preset keeps the role for the next apply', () => {
|
||||||
|
// v3.26.0: the TAR options come from the preset - without one the selector
|
||||||
|
// stays on its placeholder and the role is applied when a preset arrives
|
||||||
const els = setup();
|
const els = setup();
|
||||||
els['chain-sim-preview'].value = 'A0A4000002';
|
els['chain-sim-preview'].value = 'A0A4000002';
|
||||||
packToSp('chain-sim-preview');
|
packToSp('chain-sim-preview');
|
||||||
|
assert.strictEqual(globalThis._spTarKey, 'uiccRfm');
|
||||||
|
assert.strictEqual(els['sp-tar'].value, '', 'no preset -> no TAR options yet');
|
||||||
|
globalThis.cards = [presetFixture()];
|
||||||
|
els['sp-card-sel'].value = '0';
|
||||||
|
cardsApply(0);
|
||||||
assert.strictEqual(els['sp-tar'].value, 'B00000');
|
assert.strictEqual(els['sp-tar'].value, 'B00000');
|
||||||
els['chain-usim-preview'].value = '00A4000002';
|
|
||||||
packToSp('chain-usim-preview');
|
|
||||||
assert.strictEqual(els['sp-tar'].value, 'B00001');
|
|
||||||
els['chain-ram-preview'].value = '80E29000';
|
|
||||||
packToSp('chain-ram-preview');
|
|
||||||
assert.strictEqual(els['sp-tar'].value, '000000');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the TAR row values are left-aligned like the headers', () => {
|
test('the TAR row values are left-aligned like the headers', () => {
|
||||||
@@ -586,7 +626,8 @@ test('the TAR table renders the role rows and adds/removes free rows', () => {
|
|||||||
assert.strictEqual(_cardsTarCount, 2);
|
assert.strictEqual(_cardsTarCount, 2);
|
||||||
// the new row renders empty
|
// the new row renders empty
|
||||||
tarRow(els, 4, { tar: '', msl: '', desc: '' });
|
tarRow(els, 4, { tar: '', msl: '', desc: '' });
|
||||||
assert.deepStrictEqual(cardsFreeTarsFromForm(), [{ tar: 'AF4D02', msl: '0A', desc: 'two' }]);
|
assert.deepStrictEqual(cardsFreeTarsFromForm(),
|
||||||
|
[{ tar: 'AF4D02', msl: '0A', desc: 'two', keyset: null }]);
|
||||||
// the role rows are never removable
|
// the role rows are never removable
|
||||||
cardsTarRemove(0);
|
cardsTarRemove(0);
|
||||||
assert.strictEqual(_cardsTarCount, 2);
|
assert.strictEqual(_cardsTarCount, 2);
|
||||||
|
|||||||
@@ -43,6 +43,12 @@ code += extractFunc(html, 'cardsScp80Complete') + '\n';
|
|||||||
code += extractFunc(html, 'cardsScp81Complete') + '\n';
|
code += extractFunc(html, 'cardsScp81Complete') + '\n';
|
||||||
code += extractFunc(html, 'cardsMatchedPreset') + '\n';
|
code += extractFunc(html, 'cardsMatchedPreset') + '\n';
|
||||||
code += extractFunc(html, 'spKeysetStartReady') + '\n';
|
code += extractFunc(html, 'spKeysetStartReady') + '\n';
|
||||||
|
code += extractFunc(html, 'spKeysetGuard') + '\n';
|
||||||
|
code += extractFunc(html, 'ramKeysetGuard') + '\n';
|
||||||
|
code += extractFunc(html, 'spTarKeysetGuard') + '\n';
|
||||||
|
code += extractFunc(html, 'cardsTarEntry') + '\n';
|
||||||
|
code += extractFunc(html, 'cardsRoleTar') + '\n';
|
||||||
|
code += extractFunc(html, 'cardsRoleDefault') + '\n';
|
||||||
code += extractFunc(html, 'spPresetIdx') + '\n';
|
code += extractFunc(html, 'spPresetIdx') + '\n';
|
||||||
code += extractFunc(html, 'ramPresetIdx') + '\n';
|
code += extractFunc(html, 'ramPresetIdx') + '\n';
|
||||||
code += extractFunc(html, 'tarPresetIdx') + '\n';
|
code += extractFunc(html, 'tarPresetIdx') + '\n';
|
||||||
|
|||||||
@@ -23,8 +23,9 @@ function extractFunc(src, name) {
|
|||||||
|
|
||||||
let code = '';
|
let code = '';
|
||||||
for (const fn of ['spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
|
for (const fn of ['spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
|
||||||
'spKeysetOptionsHtml', 'spCounterTracked', 'spPresetIdx', 'ramPresetIdx',
|
'spTarOptionsHtml', 'spTarRender', 'spSpi1Seed', 'spTarKeysetGuard', 'cardsTarKeysetCell', 'cardsTarKeysetFromForm', 'cardsTarKeysetRefresh', 'cardsTarRowsRender', 'cardsTarRowHtml', 'cardsTarsFromTable', 'cardsFreeTarsFromForm', 'spCounterTracked', 'spPresetIdx', 'ramPresetIdx',
|
||||||
'tarPresetIdx', 'spKeysetSync', 'ramRender', 'esc', 'escHtml']) {
|
'tarPresetIdx', 'spKeysetSync', 'ramRender', 'esc', 'escHtml',
|
||||||
|
'cardsTarList', 'cardsTarEntry', 'cardsRoleTar']) {
|
||||||
code += extractFunc(html, fn) + '\n';
|
code += extractFunc(html, fn) + '\n';
|
||||||
}
|
}
|
||||||
code += 'function t(s){return s;}\n';
|
code += 'function t(s){return s;}\n';
|
||||||
@@ -91,20 +92,23 @@ test('spKeysetCheck enforces the keyset-number rules', () => {
|
|||||||
assert.match(spKeysetCheck(PRESET, '15', 'ZZ'), /KID/);
|
assert.match(spKeysetCheck(PRESET, '15', 'ZZ'), /KID/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('spKeysetOptionsHtml lists the keysets and marks an undefined number', () => {
|
test('spTarKeysetGuard follows the TAR default keyset (v3.26.0)', () => {
|
||||||
const html1 = spKeysetOptionsHtml(PRESET, '15', '15');
|
const preset = Object.assign({}, PRESET, { tars: [
|
||||||
assert.match(html1, /value="1" selected/);
|
{ role: 'isd', tar: '000000', msl: '16', keyset: 1 },
|
||||||
assert.match(html1, /value="2"/);
|
{ role: 'uiccRfm', tar: 'B00000', msl: '16', keyset: 2 },
|
||||||
assert.ok(html1.indexOf('0000000005') > 0, 'the counters are shown');
|
{ role: 'usimRfm', tar: 'B00001', msl: '00' },
|
||||||
// a version the preset does not define is offered but marked
|
{ tar: 'B00011', msl: '16', keyset: 3 }] });
|
||||||
const html2 = spKeysetOptionsHtml(PRESET, '35', '35');
|
assert.strictEqual(spTarKeysetGuard(preset, '000000'), '');
|
||||||
assert.match(html2, /value="3" selected/);
|
assert.strictEqual(spTarKeysetGuard(preset, 'B00001'), ''); // MSL 00: keyless is fine
|
||||||
assert.match(html2, /not defined in the preset/);
|
// a TAR naming a keyset the preset does not define
|
||||||
// a preset without keysets: the form's version shows as undefined
|
assert.match(spTarKeysetGuard(preset, 'B00011'), /is not defined in the preset/);
|
||||||
const html3 = spKeysetOptionsHtml({ keysets: [] }, '15', '15');
|
// a security TAR without a default keyset
|
||||||
assert.match(html3, /not defined in the preset/);
|
const noKeyset = Object.assign({}, PRESET, { tars: [
|
||||||
// no keyset number in the form and no keysets at all
|
{ role: 'isd', tar: '000000', msl: '16' }] });
|
||||||
assert.match(spKeysetOptionsHtml({}, '', ''), /no keyset/);
|
assert.match(spTarKeysetGuard(noKeyset, '000000'), /needs a default keyset/);
|
||||||
|
// a TAR outside the preset is the server's business; no preset: nothing
|
||||||
|
assert.strictEqual(spTarKeysetGuard(preset, 'B00099'), '');
|
||||||
|
assert.strictEqual(spTarKeysetGuard(null, '000000'), '');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the Cards form wires the keyset editor', () => {
|
test('the Cards form wires the keyset editor', () => {
|
||||||
@@ -117,9 +121,14 @@ test('the Cards form wires the keyset editor', () => {
|
|||||||
// the keyset rows are built with the KIc/KID/key/counter inputs
|
// the keyset rows are built with the KIc/KID/key/counter inputs
|
||||||
assert.match(html, /cards-ks-' \+ i \+ '-kic-key/);
|
assert.match(html, /cards-ks-' \+ i \+ '-kic-key/);
|
||||||
assert.match(html, /cards-ks-' \+ i \+ '-cntr/);
|
assert.match(html, /cards-ks-' \+ i \+ '-cntr/);
|
||||||
// the selectors exist in both SCP80 views
|
// v3.26.0: the keyset selectors are gone - the SCP80 TAR select drives
|
||||||
assert.match(html, /id="sp-keyset-sel" onchange="spKeysetChanged\(\)"/);
|
// the keyset, the RAM operations use the preset's ISD entry, and the TAR
|
||||||
assert.match(html, /id="ram-keyset-sel" onchange="ramKeysetChanged\(\)"/);
|
// table gained a keyset column
|
||||||
|
assert.match(html, /id="sp-tar" onchange="spTarChanged\(\)"/);
|
||||||
|
assert.ok(!html.includes('id="sp-keyset-sel"'));
|
||||||
|
assert.ok(!html.includes('id="ram-keyset-sel"'));
|
||||||
|
assert.ok(!html.includes('id="tar-keyset-sel"'));
|
||||||
|
assert.match(html, /id="cards-tr-' \+ i \+ '-keyset"/);
|
||||||
// the packet actions run the guard
|
// the packet actions run the guard
|
||||||
assert.ok(html.includes('const ksErr = spKeysetGuard();'));
|
assert.ok(html.includes('const ksErr = spKeysetGuard();'));
|
||||||
assert.ok(html.includes('const ksErr = ramKeysetGuard();'));
|
assert.ok(html.includes('const ksErr = ramKeysetGuard();'));
|
||||||
@@ -136,14 +145,9 @@ test('spCounterTracked follows SPI1 b5b4 (TS 102 225 5.1.1)', () => {
|
|||||||
assert.strictEqual(spCounterTracked('zz'), false);
|
assert.strictEqual(spCounterTracked('zz'), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ramRender rebuilds the keyset selector from the remembered preset', () => {
|
test('ramRender keeps the remembered preset (v3.26.0: no keyset selector)', () => {
|
||||||
// regression: the auto-selection runs before the RAM select has options, so
|
|
||||||
// its keyset list was built with no preset and stayed on "not defined"
|
|
||||||
const els = {
|
const els = {
|
||||||
'ram-card-sel': { value: '', innerHTML: '', appendChild() {} },
|
'ram-card-sel': { value: '', innerHTML: '', appendChild() {} },
|
||||||
'ram-keyset-sel': { value: '', innerHTML: '' },
|
|
||||||
'sp-keyset-sel': { value: '', innerHTML: '' },
|
|
||||||
'tar-keyset-sel': { value: '', innerHTML: '' },
|
|
||||||
'sp-kic-hex': { value: '25' },
|
'sp-kic-hex': { value: '25' },
|
||||||
'sp-kid-hex': { value: '25' },
|
'sp-kid-hex': { value: '25' },
|
||||||
};
|
};
|
||||||
@@ -160,9 +164,6 @@ test('ramRender rebuilds the keyset selector from the remembered preset', () =>
|
|||||||
ramRender();
|
ramRender();
|
||||||
assert.strictEqual(els['ram-card-sel'].value, '0',
|
assert.strictEqual(els['ram-card-sel'].value, '0',
|
||||||
'the preset select keeps the remembered card');
|
'the preset select keeps the remembered card');
|
||||||
assert.match(els['ram-keyset-sel'].innerHTML, /value="2" selected/);
|
|
||||||
assert.ok(!els['ram-keyset-sel'].innerHTML.includes('not defined'),
|
|
||||||
'the keyset selector must not show a stale "not defined" entry');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the RAM and TAR views rebuild their keyset lists when their preset is known', () => {
|
test('the RAM and TAR views rebuild their keyset lists when their preset is known', () => {
|
||||||
@@ -179,44 +180,32 @@ test('the RAM and TAR views rebuild their keyset lists when their preset is know
|
|||||||
assert.ok(html.includes("if (name === 'tar') { tarRender(); tarProbeRender(); }"));
|
assert.ok(html.includes("if (name === 'tar') { tarRender(); tarProbeRender(); }"));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the keyset selector is disabled and cleared while no preset is selected', () => {
|
test('the TAR selector waits for a preset and lists its entries', () => {
|
||||||
const els = {
|
const els = {
|
||||||
'sp-keyset-sel': { value: '', innerHTML: '' },
|
'sp-tar': { value: '', disabled: false, innerHTML: '', options: [] },
|
||||||
'ram-keyset-sel': { value: '', innerHTML: '' },
|
|
||||||
'tar-keyset-sel': { value: '', innerHTML: '' },
|
|
||||||
'sp-card-sel': { value: '' },
|
'sp-card-sel': { value: '' },
|
||||||
'ram-card-sel': { value: '' },
|
|
||||||
'tar-card-sel': { value: '' },
|
|
||||||
'sp-kic-hex': { value: '' },
|
|
||||||
'sp-kid-hex': { value: '' },
|
|
||||||
};
|
};
|
||||||
globalThis.document = { getElementById: id => els[id] || null };
|
globalThis.document = { getElementById: id => els[id] || null };
|
||||||
globalThis.cards = [];
|
globalThis.cards = [];
|
||||||
spKeysetSync();
|
spTarRender();
|
||||||
for (const id of ['sp-keyset-sel', 'ram-keyset-sel', 'tar-keyset-sel']) {
|
assert.strictEqual(els['sp-tar'].disabled, true, 'no preset: the selector waits');
|
||||||
assert.strictEqual(els[id].disabled, true, id + ' must be disabled');
|
assert.match(els['sp-tar'].innerHTML, /select a card preset/);
|
||||||
assert.match(els[id].innerHTML, /select a card preset/);
|
// a selected preset enables the selector and lists its TARs with their
|
||||||
assert.ok(!els[id].innerHTML.includes('not defined'), id);
|
// MSL and default keyset
|
||||||
}
|
globalThis.cards = [{ name: 'EP', tars: [
|
||||||
// a selected preset enables the selector and lists its keysets
|
{ role: 'isd', tar: '000000', msl: '16', keyset: 1 },
|
||||||
globalThis.cards = [{ name: 'EP', keysets: [
|
{ tar: 'AF4D02', msl: '0A', desc: 'My applet' }] }];
|
||||||
{ kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '0000000001' }] }];
|
|
||||||
els['sp-card-sel'].value = '0';
|
els['sp-card-sel'].value = '0';
|
||||||
els['ram-card-sel'].value = '0';
|
spTarRender();
|
||||||
els['tar-card-sel'].value = '0';
|
assert.strictEqual(els['sp-tar'].disabled, false);
|
||||||
els['sp-kic-hex'].value = '15';
|
assert.match(els['sp-tar'].innerHTML, /value="000000"[^>]*>000000 \u00b7 ISD \u00b7 MSL 16 \u00b7 Keyset 1</);
|
||||||
els['sp-kid-hex'].value = '15';
|
assert.match(els['sp-tar'].innerHTML, /value="AF4D02"[^>]*>AF4D02 \u00b7 My applet \u00b7 MSL 0A</);
|
||||||
spKeysetSync();
|
|
||||||
for (const id of ['sp-keyset-sel', 'ram-keyset-sel', 'tar-keyset-sel']) {
|
|
||||||
assert.strictEqual(els[id].disabled, false, id + ' must be enabled');
|
|
||||||
assert.match(els[id].innerHTML, /value="1" selected/);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('clearing the preset selection clears the keyset selectors', () => {
|
test('clearing the preset selection resets the TAR selector', () => {
|
||||||
const at = html.indexOf('function cardsApply(');
|
const at = html.indexOf('function cardsApply(');
|
||||||
assert.ok(at > 0);
|
assert.ok(at > 0);
|
||||||
const body = html.slice(at, at + 600);
|
const body = html.slice(at, at + 600);
|
||||||
assert.ok(body.includes('spKeysetSync();'),
|
assert.ok(body.includes('spTarRender(null);'),
|
||||||
'cardsApply must sync the keyset selectors when no preset is selected');
|
'cardsApply must reset the TAR selector when no preset is selected');
|
||||||
});
|
});
|
||||||
|
|||||||
+13
-10
@@ -30,9 +30,9 @@ const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamB
|
|||||||
'spCntrLow', 'ramCntrLowHtml', 'ramCntrLowPresetIdx', 'ramCntrLowView', 'ramShowCntrLow',
|
'spCntrLow', 'ramCntrLowHtml', 'ramCntrLowPresetIdx', 'ramCntrLowView', 'ramShowCntrLow',
|
||||||
'ramCntrLowBox', 'ramCntrLowClear',
|
'ramCntrLowBox', 'ramCntrLowClear',
|
||||||
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
|
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
|
||||||
'spKeysetOptionsHtml', 'spPresetIdx', 'tarPresetIdx',
|
'spTarOptionsHtml', 'spTarRender', 'spSpi1Seed', 'spTarKeysetGuard', 'cardsTarKeysetCell', 'cardsTarKeysetFromForm', 'cardsTarKeysetRefresh', 'cardsTarRowsRender', 'cardsTarRowHtml', 'cardsTarsFromTable', 'cardsFreeTarsFromForm', 'spPresetIdx', 'tarPresetIdx',
|
||||||
'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply', 'spKeysetChanged',
|
'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply',
|
||||||
'ramKeysetChanged', 'ramPresetIdx', 'spKeysetGuard', 'ramKeysetGuard',
|
'ramPresetIdx', 'spKeysetGuard', 'ramKeysetGuard',
|
||||||
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn', 'ramCapToolkitMode', 'ramOpProgressText',
|
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn', 'ramCapToolkitMode', 'ramOpProgressText',
|
||||||
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
|
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
|
||||||
let code = '';
|
let code = '';
|
||||||
@@ -1038,13 +1038,12 @@ test('every SCP80/RAM flow checks the low counter and stops', () => {
|
|||||||
assert.ok(go.includes('cardsEdit('), go);
|
assert.ok(go.includes('cardsEdit('), go);
|
||||||
assert.ok(go.includes("getElementById('cards-ks-' + row + '-cntr')"), go);
|
assert.ok(go.includes("getElementById('cards-ks-' + row + '-cntr')"), go);
|
||||||
// the notice's sync probe posts to the bounded server endpoint with the
|
// the notice's sync probe posts to the bounded server endpoint with the
|
||||||
// preset, the keyset the failed operation used and its SPI/TAR
|
// preset, the operation's TAR (v3.26.0: the RAM view uses the ISD entry,
|
||||||
|
// the server resolves the keyset) and its SPI
|
||||||
const sync = extractFunc(html, 'ramSyncCounter');
|
const sync = extractFunc(html, 'ramSyncCounter');
|
||||||
assert.ok(sync.includes("'/api/counter-probe'"), sync);
|
assert.ok(sync.includes("'/api/counter-probe'"), sync);
|
||||||
assert.ok(sync.includes("own = view === 'sp'"), sync);
|
assert.ok(sync.includes("view === 'ram' ? cardsRoleTar(preset, 'isd')"), sync);
|
||||||
assert.ok(sync.includes('kvnOf(own)'), sync);
|
assert.ok(sync.includes('spKeysetSync()'), 'the availability must refresh');
|
||||||
assert.ok(sync.includes('kvnOf(other)'), sync);
|
|
||||||
assert.ok(sync.includes('spKeysetSync()'), 'the keyset labels must refresh');
|
|
||||||
assert.ok(sync.includes('spCntrPlaceholder()'),
|
assert.ok(sync.includes('spCntrPlaceholder()'),
|
||||||
'the stored value must refresh the field placeholder (the field stays an override)');
|
'the stored value must refresh the field placeholder (the field stays an override)');
|
||||||
assert.ok(sync.includes("ramListingSpi2('40')"),
|
assert.ok(sync.includes("ramListingSpi2('40')"),
|
||||||
@@ -1372,9 +1371,13 @@ test('getRamSpParams reads the computed SPI2 byte, not the base select', () => {
|
|||||||
assert.ok(/spi2: document\.getElementById\('sp-spi2-hex'\)/.test(fn), fn);
|
assert.ok(/spi2: document\.getElementById\('sp-spi2-hex'\)/.test(fn), fn);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getRamSpParams normalises the packet TAR to three bytes', () => {
|
test('getRamSpParams sends the preset ISD TAR and no keyset fields (v3.26.0)', () => {
|
||||||
|
// the server resolves the keyset from the preset's ISD TAR entry - the
|
||||||
|
// PWA sends only the preset id, the ISD TAR, SPI and the counter override
|
||||||
const fn = extractFunc(html, 'getRamSpParams');
|
const fn = extractFunc(html, 'getRamSpParams');
|
||||||
assert.ok(/tar:[\s\S]*?padEnd\(6, '0'\)\.slice\(0, 6\),/.test(fn), fn);
|
assert.ok(fn.includes("tar: preset ? cardsRoleTar(preset, 'isd') : ''"), fn);
|
||||||
|
assert.ok(!fn.includes('kic:'), 'the keyset fields are resolved server-side');
|
||||||
|
assert.ok(!fn.includes('kicKey:'), 'the keys travel from the preset store');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ramListingSpi2 requests the SMS-submit PoR for the listing queries', () => {
|
test('ramListingSpi2 requests the SMS-submit PoR for the listing queries', () => {
|
||||||
|
|||||||
@@ -35,14 +35,20 @@ for (const fn of ['berLenStr', 'ramDeleteApdu', 'ramIncrementCntr', 'ramSaveCntr
|
|||||||
'ramCntrLowBox', 'ramCntrLowClear',
|
'ramCntrLowBox', 'ramCntrLowClear',
|
||||||
'escHtml', 'esc',
|
'escHtml', 'esc',
|
||||||
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
|
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
|
||||||
'spKeysetOptionsHtml', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender',
|
'spTarOptionsHtml', 'spTarRender', 'spSpi1Seed', 'spTarKeysetGuard', 'cardsTarKeysetCell', 'cardsTarKeysetFromForm', 'cardsTarKeysetRefresh', 'cardsTarRowsRender', 'cardsTarRowHtml', 'cardsTarsFromTable', 'cardsFreeTarsFromForm', 'cardsRoleTar', 'cardsTarEntry', 'cardsTarList', 'cardsRoleDefault', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender',
|
||||||
'cardsKeysetAdd', 'cardsKeysetRemove', 'cardsKeysetKvnUpdate',
|
'cardsKeysetAdd', 'cardsKeysetRemove', 'cardsKeysetKvnUpdate',
|
||||||
'spKeysetChanged', 'ramKeysetChanged', 'ramPresetIdx', 'spKeysetGuard',
|
'ramPresetIdx', 'spKeysetGuard',
|
||||||
'ramKeysetGuard',
|
'ramKeysetGuard',
|
||||||
]) {
|
]) {
|
||||||
code += extractFunc(html, fn) + '\n';
|
code += extractFunc(html, fn) + '\n';
|
||||||
}
|
}
|
||||||
code = 'var _cardsKeysetCount = 0;\n' + code;
|
code = 'var _cardsKeysetCount = 0;\n' + code;
|
||||||
|
// the role defaults the TAR helpers read (v3.26.0)
|
||||||
|
for (const c of ['CARDS_TAR_DEFAULTS', 'CARDS_TAR_ROLES', 'CARDS_MSL_DEFAULT']) {
|
||||||
|
const m = new RegExp('const\\s+' + c + '\\s*=\\s*([^;]+);').exec(html);
|
||||||
|
if (!m) throw new Error('const ' + c + ' not found');
|
||||||
|
code = 'var ' + c + ' = ' + m[1] + ';\n' + code;
|
||||||
|
}
|
||||||
eval(code);
|
eval(code);
|
||||||
|
|
||||||
function fakeEnv(por, success) {
|
function fakeEnv(por, success) {
|
||||||
|
|||||||
@@ -23,78 +23,43 @@ function extractFunc(src, name) {
|
|||||||
|
|
||||||
let code = '';
|
let code = '';
|
||||||
for (const fn of ['esc', 'escHtml', 'tarProbeVerdictLabel', 'tarProbeHtml',
|
for (const fn of ['esc', 'escHtml', 'tarProbeVerdictLabel', 'tarProbeHtml',
|
||||||
'tarProbeRowsHtml', 'tarProbeCustomLoad', 'tarProbeCustomSave',
|
'tarProbeRowsHtml', 'tarProbeAllRows', 'tarProbeChecked', 'cardsTarList']) {
|
||||||
'tarProbeAllRows', 'tarProbeChecked']) {
|
|
||||||
code += extractFunc(html, fn) + '\n';
|
code += extractFunc(html, fn) + '\n';
|
||||||
}
|
}
|
||||||
code += "var TAR_PROBE_TARS = JSON.parse('" +
|
|
||||||
JSON.stringify(eval('(' + html.match(/const TAR_PROBE_TARS = \[[\s\S]*?\n\];/)[0]
|
|
||||||
.replace('const TAR_PROBE_TARS = ', '').replace(/;$/, '') + ')')) + "');\n";
|
|
||||||
code += "var TAR_PROBE_STORAGE = 'simple_tar_probe';\n";
|
|
||||||
code += 'function t(s){return s;}\n';
|
code += 'function t(s){return s;}\n';
|
||||||
eval(code);
|
eval(code);
|
||||||
|
|
||||||
function fakeStorage() {
|
test('tarProbeRowsHtml renders the preset rows checked', () => {
|
||||||
const map = {};
|
// v3.26.0: the rows come from the preset - no custom rows, no remove
|
||||||
globalThis.localStorage = {
|
// buttons
|
||||||
getItem: k => (k in map ? map[k] : null),
|
|
||||||
setItem: (k, v) => { map[k] = String(v); },
|
|
||||||
};
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
test('the standard list covers the Annex D allocations', () => {
|
|
||||||
const tars = TAR_PROBE_TARS.map(e => e.tar);
|
|
||||||
assert.ok(tars.length >= 10, tars.length);
|
|
||||||
for (const want of ['000000', 'B20100', 'B00000', 'B00001', 'B00010',
|
|
||||||
'B00120', 'B00130', 'B00140', 'B20000', 'B20200', 'B20201', 'B00200']) {
|
|
||||||
assert.ok(tars.includes(want), want);
|
|
||||||
}
|
|
||||||
// every row carries the spec document it comes from
|
|
||||||
TAR_PROBE_TARS.forEach(e => {
|
|
||||||
assert.match(e.tar, /^[0-9A-F]{6}$/);
|
|
||||||
assert.ok(e.label, e.tar);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('tarProbeRowsHtml renders checked rows and marks custom ones', () => {
|
|
||||||
const out = tarProbeRowsHtml([
|
const out = tarProbeRowsHtml([
|
||||||
{ tar: '000000', label: 'Issuer Security Domain (compact)', doc: 'TS 102 226' },
|
{ tar: '000000', label: 'ISD', doc: 'MSL 16 \u00b7 Keyset 1' },
|
||||||
{ tar: '123456', label: 'my applet', custom: true },
|
{ tar: 'AF4D02', label: 'My applet' },
|
||||||
]);
|
]);
|
||||||
assert.match(out, /value="000000" checked/);
|
assert.match(out, /value="000000" checked/);
|
||||||
assert.match(out, /title="TS 102 226"/);
|
assert.match(out, /title="MSL 16/);
|
||||||
assert.match(out, /Issuer Security Domain \(compact\)/);
|
assert.match(out, /ISD/);
|
||||||
assert.match(out, /value="123456" checked/);
|
assert.match(out, /value="AF4D02" checked/);
|
||||||
assert.match(out, /tarProbeRemoveCustom\('123456'\)/, 'custom rows get a remove button');
|
assert.ok(!out.includes('tarProbeRemoveCustom'), 'no custom rows anymore');
|
||||||
assert.ok(!/tarProbeRemoveCustom\('000000'\)/.test(out), 'standard rows have none');
|
|
||||||
// escaping
|
// escaping
|
||||||
const evil = tarProbeRowsHtml([{ tar: 'AA<BB', label: '<i>x', custom: true }]);
|
const evil = tarProbeRowsHtml([{ tar: 'AA<BB', label: '<i>x' }]);
|
||||||
assert.ok(!evil.includes('AA<BB'));
|
assert.ok(!evil.includes('AA<BB'));
|
||||||
assert.ok(!evil.includes('<i>x'));
|
assert.ok(!evil.includes('<i>x'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('custom TARs round-trip through localStorage (best effort)', () => {
|
test('tarProbeAllRows lists the selected preset TARs (v3.26.0)', () => {
|
||||||
fakeStorage();
|
globalThis.cards = [{ name: 'C', tars: [
|
||||||
assert.deepStrictEqual(tarProbeCustomLoad(), []);
|
{ role: 'isd', tar: '000000', msl: '16', keyset: 1 },
|
||||||
tarProbeCustomSave([{ tar: '123456', label: 'mine' }, { tar: 'aabbcc' }]);
|
{ tar: 'AF4D02', msl: '0A', desc: 'My applet' }] }];
|
||||||
const back = tarProbeCustomLoad();
|
globalThis.tarPresetIdx = () => 0;
|
||||||
assert.deepStrictEqual(back, [{ tar: '123456', label: 'mine', custom: true },
|
assert.deepStrictEqual(tarProbeAllRows(), [
|
||||||
{ tar: 'AABBCC', label: '', custom: true }]);
|
{ tar: '000000', label: 'ISD', doc: 'MSL 16 \u00b7 Keyset 1' },
|
||||||
// garbage is dropped, never fatal
|
{ tar: 'AF4D02', label: 'My applet', doc: 'MSL 0A' },
|
||||||
localStorage.setItem('simple_tar_probe', '{not json');
|
]);
|
||||||
assert.deepStrictEqual(tarProbeCustomLoad(), []);
|
// no preset: no rows
|
||||||
localStorage.setItem('simple_tar_probe', JSON.stringify([{ tar: 'zz' }, { tar: 'B0B0B0' }]));
|
globalThis.tarPresetIdx = () => -1;
|
||||||
assert.deepStrictEqual(tarProbeCustomLoad().map(e => e.tar), ['B0B0B0']);
|
assert.deepStrictEqual(tarProbeAllRows(), []);
|
||||||
});
|
delete globalThis.tarPresetIdx;
|
||||||
|
|
||||||
test('tarProbeAllRows appends the custom TARs to the standard ones', () => {
|
|
||||||
fakeStorage();
|
|
||||||
tarProbeCustomSave([{ tar: '123456', label: 'mine' }]);
|
|
||||||
const rows = tarProbeAllRows();
|
|
||||||
assert.strictEqual(rows.length, TAR_PROBE_TARS.length + 1);
|
|
||||||
assert.strictEqual(rows[rows.length - 1].tar, '123456');
|
|
||||||
assert.strictEqual(rows[rows.length - 1].custom, true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tarProbeChecked reads the checked boxes in list order', () => {
|
test('tarProbeChecked reads the checked boxes in list order', () => {
|
||||||
@@ -141,21 +106,24 @@ test('the SCP80 tab wires the TAR probe pill', () => {
|
|||||||
assert.match(html, /<div id="scp80-sub-tar" class="hidden">/);
|
assert.match(html, /<div id="scp80-sub-tar" class="hidden">/);
|
||||||
assert.ok(html.includes("document.getElementById('scp80-sub-tar').classList.toggle('hidden', name !== 'tar');"));
|
assert.ok(html.includes("document.getElementById('scp80-sub-tar').classList.toggle('hidden', name !== 'tar');"));
|
||||||
assert.ok(html.includes("tar: 'tar-probe'"), 'the help anchor points at the new section');
|
assert.ok(html.includes("tar: 'tar-probe'"), 'the help anchor points at the new section');
|
||||||
// its own preset + keyset selectors and the warning banner
|
// its own preset selector and the warning banner; v3.26.0: the probe
|
||||||
|
// checks the preset's TAR list - no keyset selector, no custom TARs
|
||||||
assert.match(html, /id="tar-card-sel" onchange="tarApplyCard\(this.value\)"/);
|
assert.match(html, /id="tar-card-sel" onchange="tarApplyCard\(this.value\)"/);
|
||||||
assert.match(html, /id="tar-keyset-sel" onchange="tarKeysetChanged\(\)"/);
|
|
||||||
assert.match(html, /id="tar-preset-warning"/);
|
assert.match(html, /id="tar-preset-warning"/);
|
||||||
// the checklist, the add form, the APDU and the Execute button
|
assert.ok(!html.includes('id="tar-keyset-sel"'), 'the probe keyset selector is gone');
|
||||||
|
assert.ok(!html.includes('tarProbeAddCustom'), 'no arbitrary TARs anymore');
|
||||||
|
assert.match(html, /TARs to probe \(the preset's TAR list\)/);
|
||||||
|
// the checklist, the APDU and the Execute button
|
||||||
assert.match(html, /id="tar-list"/);
|
assert.match(html, /id="tar-list"/);
|
||||||
assert.match(html, /onclick="tarProbeSelectAll\(true\)"/);
|
assert.match(html, /onclick="tarProbeSelectAll\(true\)"/);
|
||||||
assert.match(html, /onclick="tarProbeAddCustom\(\)"/);
|
|
||||||
assert.match(html, /id="tar-apdu"[^>]*value="00A40000023F00"/);
|
assert.match(html, /id="tar-apdu"[^>]*value="00A40000023F00"/);
|
||||||
assert.match(html, /onclick="tarExecute\(\)"/);
|
assert.match(html, /onclick="tarExecute\(\)"/);
|
||||||
assert.match(html, /id="tar-result"/);
|
assert.match(html, /id="tar-result"/);
|
||||||
// the run
|
// the run: the preset and the checked TARs travel - the server resolves
|
||||||
|
// each row's keyset
|
||||||
assert.ok(html.includes("pysimFetch('/api/tar-probe', body)"));
|
assert.ok(html.includes("pysimFetch('/api/tar-probe', body)"));
|
||||||
assert.ok(html.includes('tars: tars'));
|
assert.ok(html.includes('tars: tars'));
|
||||||
assert.ok(html.includes("getRamSpParams('tar-card-sel')"));
|
assert.ok(!html.includes("getRamSpParams('tar-card-sel')"));
|
||||||
assert.ok(html.includes('if (data.final_cntr) ramSaveCntr(data.final_cntr);'));
|
assert.ok(html.includes('if (data.final_cntr) ramSaveCntr(data.final_cntr);'));
|
||||||
// the RAM-era leftovers are gone
|
// the RAM-era leftovers are gone
|
||||||
assert.ok(!html.includes('id="ram-tar-rows"'));
|
assert.ok(!html.includes('id="ram-tar-rows"'));
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pysim-simple-server"
|
name = "pysim-simple-server"
|
||||||
version = "3.25.8"
|
version = "3.26.0"
|
||||||
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.
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
DEFAULT_DIR = '.pysim-simple-server'
|
DEFAULT_DIR = '.pysim-simple-server'
|
||||||
DEFAULT_FILENAME = 'card_presets.json'
|
DEFAULT_FILENAME = 'card_presets.json'
|
||||||
SCHEMA_VERSION = 5
|
SCHEMA_VERSION = 6
|
||||||
|
|
||||||
# The fields a preset carries: the localStorage shape plus the id. The OTA key
|
# The fields a preset carries: the localStorage shape plus the id. The OTA key
|
||||||
# material lives in `keysets` - the b8..b5 nibble of KIc/KID numbers them,
|
# material lives in `keysets` - the b8..b5 nibble of KIc/KID numbers them,
|
||||||
@@ -88,7 +88,12 @@ DIGIT_FIELDS = ('pin1', 'puk1', 'pin2', 'puk2')
|
|||||||
KEYSET_HEX_FIELDS = ('kic', 'kid', 'kicKey', 'kidKey', 'cntr')
|
KEYSET_HEX_FIELDS = ('kic', 'kid', 'kicKey', 'kidKey', 'cntr')
|
||||||
# The TAR table: the three mandatory role entries (their values are pre-settled
|
# The TAR table: the three mandatory role entries (their values are pre-settled
|
||||||
# in the Cards form) and free entries with an optional description. Every
|
# in the Cards form) and free entries with an optional description. Every
|
||||||
# entry carries the TAR and its MSL (Minimum SPI1, one hex byte).
|
# entry carries the TAR, its MSL (Minimum SPI1, one hex byte) and its **default
|
||||||
|
# keyset** (v3.26.0): the keyset every operation addressed to that TAR uses.
|
||||||
|
# The keyset is mandatory for any non-zero MSL (the counters are keyset-linked,
|
||||||
|
# TS 102 225 A.1) and may be omitted only for MSL 00 - where setting one is
|
||||||
|
# still allowed, to pass its KIc/KID in the packet (some cards reject the
|
||||||
|
# no-security '00/00' form even when SPI declares no security).
|
||||||
TAR_ROLES = ('isd', 'uiccRfm', 'usimRfm')
|
TAR_ROLES = ('isd', 'uiccRfm', 'usimRfm')
|
||||||
TAR_ROLE_DEFAULTS = {
|
TAR_ROLE_DEFAULTS = {
|
||||||
'isd': {'tar': '000000', 'msl': '16'},
|
'isd': {'tar': '000000', 'msl': '16'},
|
||||||
@@ -194,6 +199,13 @@ def tar_msl(preset, tar_hex):
|
|||||||
return str((tar_entry(preset, tar_hex) or {}).get('msl') or '')
|
return str((tar_entry(preset, tar_hex) or {}).get('msl') or '')
|
||||||
|
|
||||||
|
|
||||||
|
def tar_keyset(preset, tar_hex):
|
||||||
|
"""The default keyset number (int 1-15) of a TAR, or None when the preset
|
||||||
|
has no such entry or the entry carries no keyset (allowed for an MSL 00
|
||||||
|
entry - the packet is then built keyless; v3.26.0)."""
|
||||||
|
return (tar_entry(preset, tar_hex) or {}).get('keyset') or None
|
||||||
|
|
||||||
|
|
||||||
class PresetError(Exception):
|
class PresetError(Exception):
|
||||||
"""A refused mutation (validation, duplicate ICCID, ...)."""
|
"""A refused mutation (validation, duplicate ICCID, ...)."""
|
||||||
|
|
||||||
@@ -306,6 +318,14 @@ class PresetStore:
|
|||||||
raw = [{k: fields.get(k) for k in LEGACY_KEY_FIELDS}]
|
raw = [{k: fields.get(k) for k in LEGACY_KEY_FIELDS}]
|
||||||
out['keysets'] = [self._normalise_keyset(ks) for ks in raw
|
out['keysets'] = [self._normalise_keyset(ks) for ks in raw
|
||||||
if isinstance(ks, dict)]
|
if isinstance(ks, dict)]
|
||||||
|
# v3.26.0: a preset with a single keyset fills every TAR entry that has
|
||||||
|
# no default keyset with it - the TARs' keysets drive the operations.
|
||||||
|
nums = [keyset_kvn(ks) for ks in out['keysets']]
|
||||||
|
nums = [n for n in nums if n]
|
||||||
|
if len(nums) == 1:
|
||||||
|
for t in out['tars']:
|
||||||
|
if not t.get('keyset'):
|
||||||
|
t['keyset'] = nums[0]
|
||||||
pid = str(fields.get('id') or '').strip().lower()
|
pid = str(fields.get('id') or '').strip().lower()
|
||||||
out['id'] = pid or uuid.uuid4().hex
|
out['id'] = pid or uuid.uuid4().hex
|
||||||
return out
|
return out
|
||||||
@@ -328,6 +348,7 @@ class PresetStore:
|
|||||||
'tar': re.sub(r'\s', '', str(t.get('tar') or '')).upper(),
|
'tar': re.sub(r'\s', '', str(t.get('tar') or '')).upper(),
|
||||||
'msl': re.sub(r'\s', '', str(t.get('msl') or '')).upper(),
|
'msl': re.sub(r'\s', '', str(t.get('msl') or '')).upper(),
|
||||||
'desc': str(t.get('desc') or '').strip(),
|
'desc': str(t.get('desc') or '').strip(),
|
||||||
|
'keyset': self._normalise_keyset_ref(t.get('keyset')),
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
msl = re.sub(r'\s', '', str(fields.get('spi1') or '')).upper()
|
msl = re.sub(r'\s', '', str(fields.get('spi1') or '')).upper()
|
||||||
@@ -338,6 +359,7 @@ class PresetStore:
|
|||||||
'tar': re.sub(r'\s', '', str(fields.get(src) or '')).upper(),
|
'tar': re.sub(r'\s', '', str(fields.get(src) or '')).upper(),
|
||||||
'msl': msl,
|
'msl': msl,
|
||||||
'desc': '',
|
'desc': '',
|
||||||
|
'keyset': None,
|
||||||
})
|
})
|
||||||
out = []
|
out = []
|
||||||
for role in TAR_ROLES:
|
for role in TAR_ROLES:
|
||||||
@@ -346,16 +368,37 @@ class PresetStore:
|
|||||||
out.append({'role': role,
|
out.append({'role': role,
|
||||||
'tar': (cur or {}).get('tar') or d['tar'],
|
'tar': (cur or {}).get('tar') or d['tar'],
|
||||||
'msl': (cur or {}).get('msl') or d['msl'],
|
'msl': (cur or {}).get('msl') or d['msl'],
|
||||||
'desc': ''})
|
'desc': '',
|
||||||
|
'keyset': (cur or {}).get('keyset')})
|
||||||
for e in entries:
|
for e in entries:
|
||||||
if e['role']:
|
if e['role']:
|
||||||
continue
|
continue
|
||||||
if not (e['tar'] or e['msl'] or e['desc']):
|
if not (e['tar'] or e['msl'] or e['desc'] or e.get('keyset')):
|
||||||
continue # a blank editor row
|
continue # a blank editor row
|
||||||
# free entries carry no `role`: the three roles are the fixed ones
|
# free entries carry no `role`: the three roles are the fixed ones
|
||||||
out.append({'tar': e['tar'], 'msl': e['msl'], 'desc': e['desc']})
|
out.append({'tar': e['tar'], 'msl': e['msl'], 'desc': e['desc'],
|
||||||
|
'keyset': e.get('keyset')})
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalise_keyset_ref(v):
|
||||||
|
"""A TAR's default keyset number (int 1-15) or None. Accepts an int
|
||||||
|
and the decimal ('1'..'15') or hex-ish ('01'..'0F') string forms; an
|
||||||
|
unusable value becomes None - the validation reports a missing keyset
|
||||||
|
where one is mandatory (v3.26.0)."""
|
||||||
|
if v is None or v == '':
|
||||||
|
return None
|
||||||
|
s = str(v).strip().upper()
|
||||||
|
if re.fullmatch(r'[0-9]{1,2}', s):
|
||||||
|
n = int(s, 10)
|
||||||
|
if 1 <= n <= KVN_MAX:
|
||||||
|
return n
|
||||||
|
if re.fullmatch(r'[0-9A-F]{1,2}', s):
|
||||||
|
n = int(s, 16)
|
||||||
|
if 1 <= n <= KVN_MAX:
|
||||||
|
return n
|
||||||
|
return None
|
||||||
|
|
||||||
def _normalise_keyset(self, ks):
|
def _normalise_keyset(self, ks):
|
||||||
"""One keyset, cleaned. The counter is kept as typed here and
|
"""One keyset, cleaned. The counter is kept as typed here and
|
||||||
validated + padded in _validate_keyset() - a bad value must be
|
validated + padded in _validate_keyset() - a bad value must be
|
||||||
@@ -402,6 +445,15 @@ class PresetStore:
|
|||||||
'byte, TS 102 226 8.2.1.3.2.4)' % t['tar'])
|
'byte, TS 102 226 8.2.1.3.2.4)' % t['tar'])
|
||||||
if not re.fullmatch(r'[0-9A-F]{2}', t['msl']):
|
if not re.fullmatch(r'[0-9A-F]{2}', t['msl']):
|
||||||
raise PresetError('MSL must be one hex byte: %r' % t['msl'])
|
raise PresetError('MSL must be one hex byte: %r' % t['msl'])
|
||||||
|
if t.get('keyset'):
|
||||||
|
if not any(keyset_kvn(ks) == t['keyset'] for ks in p['keysets']):
|
||||||
|
raise PresetError('TAR %s names keyset %02X, which this '
|
||||||
|
'preset does not define'
|
||||||
|
% (t['tar'], t['keyset']))
|
||||||
|
elif t['msl'] != '00':
|
||||||
|
raise PresetError('TAR %s needs a default keyset (its MSL %s '
|
||||||
|
'requires security) - set it in the Cards tab'
|
||||||
|
% (t['tar'], t['msl']))
|
||||||
if t['tar'] in seen_tars:
|
if t['tar'] in seen_tars:
|
||||||
raise PresetError('duplicate TAR %s in this preset' % t['tar'])
|
raise PresetError('duplicate TAR %s in this preset' % t['tar'])
|
||||||
seen_tars.add(t['tar'])
|
seen_tars.add(t['tar'])
|
||||||
|
|||||||
+236
-79
@@ -36,7 +36,7 @@ from osmocom.tlv import BER_TLV_IE
|
|||||||
from osmocom.utils import rpad
|
from osmocom.utils import rpad
|
||||||
|
|
||||||
|
|
||||||
VERSION = '3.25.8'
|
VERSION = '3.26.0'
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -1700,6 +1700,54 @@ def _preset_keyset_check(server, preset_id, kic, kid):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _preset_tar_keyset(server, preset_id, tar):
|
||||||
|
"""The default keyset number of a TAR in the named preset (v3.26.0), or
|
||||||
|
None (no preset, no such TAR entry, or an MSL 00 entry without one)."""
|
||||||
|
preset = _preset_by_id(server, preset_id)
|
||||||
|
if preset is None:
|
||||||
|
return None
|
||||||
|
return presets.tar_keyset(preset, tar)
|
||||||
|
|
||||||
|
|
||||||
|
def _preset_keyset_by_kvn(preset, kvn):
|
||||||
|
"""The preset's keyset dict with number `kvn`, or None."""
|
||||||
|
return next((ks for ks in _preset_keysets(preset)
|
||||||
|
if presets.keyset_kvn(ks) == kvn), None)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_operation_keyset(server, body, tar):
|
||||||
|
"""Fill an operation's keyset fields from the preset's default keyset for
|
||||||
|
`tar` (v3.26.0).
|
||||||
|
|
||||||
|
An explicit KIc/KID in the request is an API override and stays as it is;
|
||||||
|
otherwise the TAR's default keyset supplies kic/kid/kicKey/kidKey. A TAR
|
||||||
|
whose MSL requires security must have one - the operation refuses instead
|
||||||
|
of sending a keyless packet the card would reject (or, worse, process as
|
||||||
|
no-security). Returns ``(kvn, error)``."""
|
||||||
|
explicit_kic = str((body or {}).get('kic') or '').strip().upper()
|
||||||
|
explicit_kid = str((body or {}).get('kid') or '').strip().upper()
|
||||||
|
if explicit_kic or explicit_kid:
|
||||||
|
return (_kvn_of(explicit_kic, explicit_kid)[0] or None), None
|
||||||
|
preset = _preset_by_id(server, (body or {}).get('preset_id'))
|
||||||
|
if preset is None:
|
||||||
|
return None, None
|
||||||
|
kvn = presets.tar_keyset(preset, tar)
|
||||||
|
if not kvn:
|
||||||
|
msl = presets.tar_msl(preset, tar)
|
||||||
|
if msl and msl != '00':
|
||||||
|
return None, ('TAR %s needs a default keyset (its MSL %s requires '
|
||||||
|
'security) - set it in the Cards tab' % (tar, msl))
|
||||||
|
return None, None
|
||||||
|
keyset = _preset_keyset_by_kvn(preset, kvn)
|
||||||
|
if keyset is None:
|
||||||
|
return None, ('keyset %02X is not defined in preset %s - add it in the '
|
||||||
|
'Cards tab' % (kvn, preset.get('name') or
|
||||||
|
(body or {}).get('preset_id')))
|
||||||
|
for field in ('kic', 'kid', 'kicKey', 'kidKey'):
|
||||||
|
body[field] = str(keyset.get(field) or '').strip()
|
||||||
|
return kvn, None
|
||||||
|
|
||||||
|
|
||||||
def _preset_counter_persist(server, preset_id, cntr, source, kvn=None):
|
def _preset_counter_persist(server, preset_id, cntr, source, kvn=None):
|
||||||
"""Persist the counter an operation accepted into the server-side preset
|
"""Persist the counter an operation accepted into the server-side preset
|
||||||
store (v3.8.0), into the keyset it used (each keyset has its own dedicated
|
store (v3.8.0), into the keyset it used (each keyset has its own dedicated
|
||||||
@@ -1916,6 +1964,7 @@ def _counter_probe_params(preset, body):
|
|||||||
keysets = _preset_keysets(preset)
|
keysets = _preset_keysets(preset)
|
||||||
if not keysets:
|
if not keysets:
|
||||||
raise ValueError('the preset defines no keyset')
|
raise ValueError('the preset defines no keyset')
|
||||||
|
tar = str(body.get('tar') or presets.role_tar(preset, 'isd') or '').strip().upper()
|
||||||
want = body.get('kvn')
|
want = body.get('kvn')
|
||||||
if want not in (None, ''):
|
if want not in (None, ''):
|
||||||
try:
|
try:
|
||||||
@@ -1926,7 +1975,13 @@ def _counter_probe_params(preset, body):
|
|||||||
if keyset is None:
|
if keyset is None:
|
||||||
raise ValueError('keyset %d is not defined in the preset' % want)
|
raise ValueError('keyset %d is not defined in the preset' % want)
|
||||||
else:
|
else:
|
||||||
keyset = keysets[0]
|
# v3.26.0: the TAR's default keyset, else the first - the probe is a
|
||||||
|
# repair tool and keeps working for a legacy preset without TAR keysets.
|
||||||
|
tar_kvn = presets.tar_keyset(preset, tar)
|
||||||
|
keyset = (next((k for k in keysets if presets.keyset_kvn(k) == tar_kvn), None)
|
||||||
|
if tar_kvn else None)
|
||||||
|
if keyset is None:
|
||||||
|
keyset = keysets[0]
|
||||||
kic = str(keyset.get('kic') or '').strip().upper()
|
kic = str(keyset.get('kic') or '').strip().upper()
|
||||||
kid = str(keyset.get('kid') or '').strip().upper()
|
kid = str(keyset.get('kid') or '').strip().upper()
|
||||||
kic_key = str(keyset.get('kicKey') or '').strip()
|
kic_key = str(keyset.get('kicKey') or '').strip()
|
||||||
@@ -1953,7 +2008,6 @@ def _counter_probe_params(preset, body):
|
|||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
raise ValueError('invalid attempt budget')
|
raise ValueError('invalid attempt budget')
|
||||||
max_attempts = max(1, min(64, max_attempts))
|
max_attempts = max(1, min(64, max_attempts))
|
||||||
tar = str(body.get('tar') or presets.role_tar(preset, 'isd') or '').strip().upper()
|
|
||||||
spi1, _ = _spi1_for_tar(preset, tar, body.get('spi1'), prefer_msl=True)
|
spi1, _ = _spi1_for_tar(preset, tar, body.get('spi1'), prefer_msl=True)
|
||||||
if not _counter_tracked(spi1):
|
if not _counter_tracked(spi1):
|
||||||
raise ValueError('the probe needs a counter check - SPI1 %s has none '
|
raise ValueError('the probe needs a counter check - SPI1 %s has none '
|
||||||
@@ -6133,26 +6187,6 @@ _CARD_FREE_GET = frozenset([
|
|||||||
'/api/scp81/log', '/api/bip/log',
|
'/api/scp81/log', '/api/bip/log',
|
||||||
])
|
])
|
||||||
|
|
||||||
# TAR values allocated by ETSI (TS 101 220 V18.3.0 Annex D, Table D.1): the
|
|
||||||
# TAR probe sends one harmless command to each and reports the card's answer.
|
|
||||||
# 'B00200' is not an application - it is the start of the RFU range, kept as a
|
|
||||||
# control (a card must not answer it).
|
|
||||||
TAR_PROBE_TARS = [
|
|
||||||
('000000', 'Issuer Security Domain (compact)'),
|
|
||||||
('B20100', 'Issuer Security Domain (expanded)'),
|
|
||||||
('B00000', 'UICC shared file system RFM (compact)'),
|
|
||||||
('B00001', 'ADF RFM (compact)'),
|
|
||||||
('B00010', 'SIM file system RFM (compact)'),
|
|
||||||
('B00120', 'UICC shared file system RFM (expanded)'),
|
|
||||||
('B00130', 'SIM file system RFM (expanded)'),
|
|
||||||
('B00140', 'ADF RFM (expanded)'),
|
|
||||||
('B20000', 'USAT interpreter'),
|
|
||||||
('B20200', 'Multiplexing application'),
|
|
||||||
('B20201', 'Controlling Authority Security Domain'),
|
|
||||||
('B20202', 'OMA BCAST audience measurement'),
|
|
||||||
('B20203', 'OMA DM LWM2M application'),
|
|
||||||
('B00200', 'RFU (control)'),
|
|
||||||
]
|
|
||||||
TAR_PROBE_DEFAULT_APDU = '00A40000023F00' # SELECT MF: harmless, applet-visible
|
TAR_PROBE_DEFAULT_APDU = '00A40000023F00' # SELECT MF: harmless, applet-visible
|
||||||
|
|
||||||
# The TARs the tool treats as remote-management targets: the preset's role
|
# The TARs the tool treats as remote-management targets: the preset's role
|
||||||
@@ -6447,6 +6481,24 @@ def _test_preset_error(script, preset):
|
|||||||
if not _keyset_complete(ks):
|
if not _keyset_complete(ks):
|
||||||
return ('step %d: keyset %d is incomplete (KIc, KID, both '
|
return ('step %d: keyset %d is incomplete (KIc, KID, both '
|
||||||
'keys and a counter)' % (i + 1, want))
|
'keys and a counter)' % (i + 1, want))
|
||||||
|
else:
|
||||||
|
# v3.26.0: the step's TAR picks its default keyset - it must exist
|
||||||
|
# and be complete; a TAR whose MSL requires security must have one.
|
||||||
|
tar = p.get('tar') or presets.role_tar(preset, 'isd')
|
||||||
|
tar_kvn = presets.tar_keyset(preset, tar)
|
||||||
|
if tar_kvn:
|
||||||
|
ks = next((k for k in keysets if presets.keyset_kvn(k) == tar_kvn), None)
|
||||||
|
if ks is None:
|
||||||
|
return ('step %d: keyset %d (the default of TAR %s) is not '
|
||||||
|
'defined in the preset' % (i + 1, tar_kvn, tar))
|
||||||
|
if not _keyset_complete(ks):
|
||||||
|
return ('step %d: keyset %d (the default of TAR %s) is '
|
||||||
|
'incomplete (KIc, KID, both keys and a counter)'
|
||||||
|
% (i + 1, tar_kvn, tar))
|
||||||
|
elif (presets.tar_msl(preset, tar) or '') not in ('', '00'):
|
||||||
|
return ('step %d: TAR %s needs a default keyset (its MSL %s '
|
||||||
|
'requires security) - set it in the Cards tab'
|
||||||
|
% (i + 1, tar, presets.tar_msl(preset, tar)))
|
||||||
tar = p.get('tar') or presets.role_tar(preset, 'isd')
|
tar = p.get('tar') or presets.role_tar(preset, 'isd')
|
||||||
if not tar:
|
if not tar:
|
||||||
return 'step %d: no TAR (neither in the step nor in the preset)' % (i + 1)
|
return 'step %d: no TAR (neither in the step nor in the preset)' % (i + 1)
|
||||||
@@ -6781,8 +6833,10 @@ def _test_run_scp80(server, step, ctx):
|
|||||||
preset = ctx['preset']
|
preset = ctx['preset']
|
||||||
p = step['params']
|
p = step['params']
|
||||||
# The keyset: a step may name a keyset number (`kvn`), otherwise the
|
# The keyset: a step may name a keyset number (`kvn`), otherwise the
|
||||||
# preset's first keyset is used (each keyset has its own keys and counter -
|
# default keyset of the step's TAR is used (v3.26.0; each keyset has its
|
||||||
# TS 102 225 Annex A.1).
|
# own keys and counter - TS 102 225 Annex A.1). A TAR whose MSL requires
|
||||||
|
# security must have one - the counters are keyset-linked; an MSL 00 step
|
||||||
|
# falls back to the preset's first keyset.
|
||||||
keysets = _preset_keysets(preset)
|
keysets = _preset_keysets(preset)
|
||||||
keyset = None
|
keyset = None
|
||||||
if p.get('kvn') is not None:
|
if p.get('kvn') is not None:
|
||||||
@@ -6792,7 +6846,22 @@ def _test_run_scp80(server, step, ctx):
|
|||||||
raise testscript.ScriptError('keyset %d is not defined in the '
|
raise testscript.ScriptError('keyset %d is not defined in the '
|
||||||
'selected card preset' % want)
|
'selected card preset' % want)
|
||||||
elif keysets:
|
elif keysets:
|
||||||
keyset = keysets[0]
|
tar = str(p.get('tar') or presets.role_tar(preset, 'isd') or '').strip().upper()
|
||||||
|
tar_kvn = presets.tar_keyset(preset, tar)
|
||||||
|
if tar_kvn:
|
||||||
|
keyset = next((ks for ks in keysets
|
||||||
|
if presets.keyset_kvn(ks) == tar_kvn), None)
|
||||||
|
if keyset is None:
|
||||||
|
raise testscript.ScriptError(
|
||||||
|
'keyset %d (the default of TAR %s) is not defined in the '
|
||||||
|
'selected card preset' % (tar_kvn, tar))
|
||||||
|
else:
|
||||||
|
msl = presets.tar_msl(preset, tar)
|
||||||
|
if msl and msl != '00':
|
||||||
|
raise testscript.ScriptError(
|
||||||
|
'TAR %s needs a default keyset (its MSL %s requires '
|
||||||
|
'security) - set it in the Cards tab' % (tar, msl))
|
||||||
|
keyset = keysets[0]
|
||||||
kic = p.get('kic') or (keyset or {}).get('kic') or ''
|
kic = p.get('kic') or (keyset or {}).get('kic') or ''
|
||||||
kid = p.get('kid') or (keyset or {}).get('kid') or ''
|
kid = p.get('kid') or (keyset or {}).get('kid') or ''
|
||||||
kic_key = p.get('kicKey') or (keyset or {}).get('kicKey') or ''
|
kic_key = p.get('kicKey') or (keyset or {}).get('kicKey') or ''
|
||||||
@@ -8043,6 +8112,23 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
err = _preset_keyset_check(self.server, body.get('preset_id'), kic, kid)
|
err = _preset_keyset_check(self.server, body.get('preset_id'), kic, kid)
|
||||||
return (kvn or None), err
|
return (kvn or None), err
|
||||||
|
|
||||||
|
def _op_keyset(self, body, tar=None):
|
||||||
|
"""v3.26.0: resolve an operation's keyset from the preset's TAR table
|
||||||
|
(the RAM operations default to the ISD role's TAR), then run the
|
||||||
|
KIc/KID checks. An explicit request KIc/KID stays an API override.
|
||||||
|
Returns ``(kvn, kic, kid, error)`` with the effective values."""
|
||||||
|
t = str(tar if tar is not None else (body.get('tar') or '')).strip().upper()
|
||||||
|
if not t and body.get('preset_id'):
|
||||||
|
preset = _preset_by_id(self.server, body.get('preset_id'))
|
||||||
|
t = presets.role_tar(preset, 'isd') if preset else ''
|
||||||
|
_kvn, err = _resolve_operation_keyset(self.server, body, t)
|
||||||
|
if err:
|
||||||
|
return None, '', '', err
|
||||||
|
kic = str(body.get('kic') or '').strip().upper()
|
||||||
|
kid = str(body.get('kid') or '').strip().upper()
|
||||||
|
kvn, err = self._keyset_guard(body, kic, kid)
|
||||||
|
return kvn, kic, kid, err
|
||||||
|
|
||||||
def _log_req(self, body=None):
|
def _log_req(self, body=None):
|
||||||
if self.server.log_requests:
|
if self.server.log_requests:
|
||||||
if body is not None:
|
if body is not None:
|
||||||
@@ -9216,11 +9302,15 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
# deliberately ask for more security than the TAR's minimum), with
|
# deliberately ask for more security than the TAR's minimum), with
|
||||||
# a warning below the TAR's MSL; without one it is derived from the
|
# a warning below the TAR's MSL; without one it is derived from the
|
||||||
# MSL of the packet's TAR (TS 102 226 8.2.1.3.2.4 - the card checks
|
# MSL of the packet's TAR (TS 102 226 8.2.1.3.2.4 - the card checks
|
||||||
# the MSL before the security processing).
|
# the MSL before the security processing). An APDU send without a
|
||||||
|
# TAR uses the preset's ISD role entry (v3.26.0).
|
||||||
try:
|
try:
|
||||||
spi1, spi_warn = _spi1_for_tar(
|
preset_ref = _preset_by_id(self.server, body.get('preset_id'))
|
||||||
_preset_by_id(self.server, body.get('preset_id')),
|
tar_ref = str(body.get('tar') or '').strip().upper()
|
||||||
str(body.get('tar') or '').strip().upper(), body.get('spi1'))
|
if not tar_ref and apdu and preset_ref:
|
||||||
|
tar_ref = presets.role_tar(preset_ref, 'isd')
|
||||||
|
spi1, spi_warn = _spi1_for_tar(preset_ref, tar_ref,
|
||||||
|
body.get('spi1'))
|
||||||
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)
|
||||||
@@ -9245,9 +9335,15 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
# RAM operation: SCP80-wrap the raw GP command. The packet
|
# RAM operation: SCP80-wrap the raw GP command. The packet
|
||||||
# may exceed one SMS (pySim refuses that), so use our own
|
# may exceed one SMS (pySim refuses that), so use our own
|
||||||
# encoder and let _send_secured_packet segment it.
|
# encoder and let _send_secured_packet segment it.
|
||||||
kic = body.get('kic', '25')
|
|
||||||
kid = body.get('kid', '25')
|
|
||||||
tar = body.get('tar', '000000')
|
tar = body.get('tar', '000000')
|
||||||
|
# The keyset: the preset's default for the packet's TAR
|
||||||
|
# (v3.26.0) unless the request names KIc/KID itself.
|
||||||
|
kvn, kic, kid, keyset_err = self._op_keyset(body, tar)
|
||||||
|
if keyset_err:
|
||||||
|
resp = {'error': keyset_err}
|
||||||
|
self._send_json(resp, 400)
|
||||||
|
self._log_resp(resp)
|
||||||
|
return
|
||||||
kic_key = body.get('kicKey', '')
|
kic_key = body.get('kicKey', '')
|
||||||
kid_key = body.get('kidKey', '')
|
kid_key = body.get('kidKey', '')
|
||||||
# The counter: an explicit `cntr` wins; otherwise the
|
# The counter: an explicit `cntr` wins; otherwise the
|
||||||
@@ -9494,26 +9590,26 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
self._log_resp(err)
|
self._log_resp(err)
|
||||||
return
|
return
|
||||||
spi2 = body.get('spi2', '01')
|
spi2 = body.get('spi2', '01')
|
||||||
kic = body.get('kic', '25')
|
|
||||||
kid = body.get('kid', '25')
|
|
||||||
cntr = body.get('cntr', '00000000')
|
cntr = body.get('cntr', '00000000')
|
||||||
kic_key = body.get('kicKey', '')
|
|
||||||
kid_key = body.get('kidKey', '')
|
|
||||||
sd_aid = body.get('sd_aid', '').replace(' ', '')
|
sd_aid = body.get('sd_aid', '').replace(' ', '')
|
||||||
install_params_hex = body.get('install_params', '').replace(' ', '')
|
install_params_hex = body.get('install_params', '').replace(' ', '')
|
||||||
stk_params_hex = body.get('stk_params', '').replace(' ', '')
|
stk_params_hex = body.get('stk_params', '').replace(' ', '')
|
||||||
make_selectable = body.get('make_selectable', True)
|
make_selectable = body.get('make_selectable', True)
|
||||||
privileges_hex = body.get('privileges', '').replace(' ', '') or '00'
|
privileges_hex = body.get('privileges', '').replace(' ', '') or '00'
|
||||||
|
|
||||||
# The keyset number must be consistent and defined in the preset
|
# The keyset: the preset's default for the operation's TAR (the
|
||||||
# (TS 102 225 A.2 / the "keyset must exist" rule) - checked
|
# ISD role entry; v3.26.0) unless the request names KIc/KID
|
||||||
# before the format probe consumes a counter.
|
# itself - then the number must be consistent and defined in
|
||||||
kvn, keyset_err = self._keyset_guard(body, kic, kid)
|
# the preset (TS 102 225 A.2 / the "keyset must exist" rule),
|
||||||
|
# checked before the format probe consumes a counter.
|
||||||
|
kvn, kic, kid, keyset_err = self._op_keyset(body)
|
||||||
if keyset_err:
|
if keyset_err:
|
||||||
err = {'success': False, 'error': keyset_err}
|
err = {'success': False, 'error': keyset_err}
|
||||||
self._send_json(err, 400)
|
self._send_json(err, 400)
|
||||||
self._log_resp(err)
|
self._log_resp(err)
|
||||||
return
|
return
|
||||||
|
kic_key = body.get('kicKey', '')
|
||||||
|
kid_key = body.get('kidKey', '')
|
||||||
|
|
||||||
# The counter: explicit `cntr` wins, else the preset's value
|
# The counter: explicit `cntr` wins, else the preset's value
|
||||||
# for this keyset (the server owns the counters, v3.20.0).
|
# for this keyset (the server owns the counters, v3.20.0).
|
||||||
@@ -9707,19 +9803,23 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
return
|
return
|
||||||
sp_state = {
|
sp_state = {
|
||||||
'spi1': spi1, 'spi2': body.get('spi2', '01'),
|
'spi1': spi1, 'spi2': body.get('spi2', '01'),
|
||||||
'kic': body.get('kic', '25'), 'kid': body.get('kid', '25'),
|
|
||||||
'tar': tar, 'cntr': body.get('cntr', '00000000'),
|
'tar': tar, 'cntr': body.get('cntr', '00000000'),
|
||||||
'kic_key': body.get('kicKey', ''), 'kid_key': body.get('kidKey', ''),
|
|
||||||
'include_cpi': body.get('includeCpi', True)}
|
'include_cpi': body.get('includeCpi', True)}
|
||||||
# The keyset number must be consistent and defined in the preset
|
# The keyset: the preset's default for the operation's TAR
|
||||||
|
# (v3.26.0) unless the request names KIc/KID itself - then the
|
||||||
|
# number must be consistent and defined in the preset
|
||||||
# (TS 102 225 A.2 / the "keyset must exist" rule) - checked
|
# (TS 102 225 A.2 / the "keyset must exist" rule) - checked
|
||||||
# before the format probe consumes a counter.
|
# before the format probe consumes a counter.
|
||||||
kvn, keyset_err = self._keyset_guard(body, sp_state['kic'], sp_state['kid'])
|
kvn, kic, kid, keyset_err = self._op_keyset(body, tar)
|
||||||
if keyset_err:
|
if keyset_err:
|
||||||
err = {'success': False, 'error': keyset_err}
|
err = {'success': False, 'error': keyset_err}
|
||||||
self._send_json(err, 400)
|
self._send_json(err, 400)
|
||||||
self._log_resp(err)
|
self._log_resp(err)
|
||||||
return
|
return
|
||||||
|
sp_state['kic'] = kic
|
||||||
|
sp_state['kid'] = kid
|
||||||
|
sp_state['kic_key'] = body.get('kicKey', '')
|
||||||
|
sp_state['kid_key'] = body.get('kidKey', '')
|
||||||
# The counter: explicit `cntr` wins, else the preset's value
|
# The counter: explicit `cntr` wins, else the preset's value
|
||||||
# for this keyset (the server owns the counters, v3.20.0).
|
# for this keyset (the server owns the counters, v3.20.0).
|
||||||
sp_state['cntr'] = _preset_counter_seed(self.server, body, kvn) or sp_state['cntr']
|
sp_state['cntr'] = _preset_counter_seed(self.server, body, kvn) or sp_state['cntr']
|
||||||
@@ -9832,18 +9932,19 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
self._log_resp(err)
|
self._log_resp(err)
|
||||||
return
|
return
|
||||||
spi2 = body.get('spi2', '01')
|
spi2 = body.get('spi2', '01')
|
||||||
kic = body.get('kic', '25')
|
|
||||||
kid = body.get('kid', '25')
|
|
||||||
cntr = body.get('cntr', '00000000')
|
cntr = body.get('cntr', '00000000')
|
||||||
kic_key = body.get('kicKey', '')
|
|
||||||
kid_key = body.get('kidKey', '')
|
|
||||||
steps = []
|
steps = []
|
||||||
kvn, keyset_err = self._keyset_guard(body, kic, kid)
|
# The keyset: the preset's default for the operation's TAR
|
||||||
|
# (the ISD role entry; v3.26.0) unless the request names
|
||||||
|
# KIc/KID itself.
|
||||||
|
kvn, kic, kid, keyset_err = self._op_keyset(body)
|
||||||
if keyset_err:
|
if keyset_err:
|
||||||
err = {'success': False, 'error': keyset_err}
|
err = {'success': False, 'error': keyset_err}
|
||||||
self._send_json(err, 400)
|
self._send_json(err, 400)
|
||||||
self._log_resp(err)
|
self._log_resp(err)
|
||||||
return
|
return
|
||||||
|
kic_key = body.get('kicKey', '')
|
||||||
|
kid_key = body.get('kidKey', '')
|
||||||
# The counter: explicit `cntr` wins, else the preset's value
|
# The counter: explicit `cntr` wins, else the preset's value
|
||||||
# for this keyset (the server owns the counters, v3.20.0).
|
# for this keyset (the server owns the counters, v3.20.0).
|
||||||
cntr = _preset_counter_seed(self.server, body, kvn) or cntr
|
cntr = _preset_counter_seed(self.server, body, kvn) or cntr
|
||||||
@@ -10487,40 +10588,59 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
return
|
return
|
||||||
body = self._read_body()
|
body = self._read_body()
|
||||||
self._log_req(body)
|
self._log_req(body)
|
||||||
kic = body.get('kic', '')
|
# v3.26.0: the probe checks the preset's TAR list only - no
|
||||||
kid = body.get('kid', '')
|
# arbitrary TARs - and each row uses its TAR's default keyset.
|
||||||
kvn, keyset_err = self._keyset_guard(body, kic, kid)
|
if not body.get('preset_id'):
|
||||||
if keyset_err:
|
err = {'success': False,
|
||||||
err = {'success': False, 'error': keyset_err}
|
'error': 'preset_id is required - the probe checks the '
|
||||||
|
'preset TAR list'}
|
||||||
|
self._send_json(err, 400)
|
||||||
|
self._log_resp(err)
|
||||||
|
return
|
||||||
|
preset = _preset_by_id(self.server, body.get('preset_id'))
|
||||||
|
if not preset:
|
||||||
|
err = {'success': False, 'error': 'unknown preset'}
|
||||||
self._send_json(err, 400)
|
self._send_json(err, 400)
|
||||||
self._log_resp(err)
|
self._log_resp(err)
|
||||||
return
|
return
|
||||||
# The probe sends real secured packets: it needs a counter check
|
# The probe sends real secured packets: it needs a counter check
|
||||||
# (every accepted packet advances and is persisted) and a PoR. Each
|
# (every accepted packet advances and is persisted) and a PoR.
|
||||||
# TAR uses its own MSL when the preset carries it; the caller's SPI1
|
# Each TAR uses its own MSL and its own default keyset.
|
||||||
# is the fallback for the arbitrary TARs the checklist probes.
|
|
||||||
spi1_base = str(body.get('spi1') or '').strip().upper()
|
spi1_base = str(body.get('spi1') or '').strip().upper()
|
||||||
spi2 = str(body.get('spi2') or '01').strip().upper()
|
spi2 = str(body.get('spi2') or '01').strip().upper()
|
||||||
preset = _preset_by_id(self.server, body.get('preset_id'))
|
|
||||||
apdu = (body.get('apdu') or TAR_PROBE_DEFAULT_APDU).replace(' ', '').upper()
|
apdu = (body.get('apdu') or TAR_PROBE_DEFAULT_APDU).replace(' ', '').upper()
|
||||||
labels = {tar: label for tar, label in TAR_PROBE_TARS}
|
role_label = {'isd': 'ISD', 'uiccRfm': 'UICC RFM', 'usimRfm': 'ADF RFM'}
|
||||||
|
by_tar = {}
|
||||||
|
for t in (preset.get('tars') or []):
|
||||||
|
tar = str(t.get('tar') or '').strip().upper()
|
||||||
|
if tar:
|
||||||
|
by_tar[tar] = t
|
||||||
wanted = body.get('tars')
|
wanted = body.get('tars')
|
||||||
if isinstance(wanted, list) and wanted:
|
if isinstance(wanted, list) and wanted:
|
||||||
entries = []
|
entries = []
|
||||||
for t in wanted:
|
for t in wanted:
|
||||||
tar = str(t).replace(' ', '').upper()
|
tar = str(t).replace(' ', '').upper()
|
||||||
entries.append((tar, labels.get(tar, '')))
|
e = by_tar.get(tar)
|
||||||
|
if e is None:
|
||||||
|
err = {'success': False,
|
||||||
|
'error': 'TAR %s is not in the preset - the probe '
|
||||||
|
'checks the preset TAR list only' % tar}
|
||||||
|
self._send_json(err, 400)
|
||||||
|
self._log_resp(err)
|
||||||
|
return
|
||||||
|
entries.append((tar, e))
|
||||||
else:
|
else:
|
||||||
entries = list(TAR_PROBE_TARS)
|
entries = [(tar, e) for tar, e in by_tar.items()]
|
||||||
# Resolve every TAR's SPI1 before the first packet is sent: a
|
# Resolve every TAR's SPI1 and keyset before the first packet is
|
||||||
# missing MSL must refuse the whole probe, not leave it half-run.
|
# sent: a missing MSL or keyset must refuse the whole probe, not
|
||||||
|
# leave it half-run.
|
||||||
resolved = []
|
resolved = []
|
||||||
for tar, label in entries:
|
for tar, e in entries:
|
||||||
try:
|
try:
|
||||||
tar_spi1, _ = _spi1_for_tar(preset, tar, spi1_base,
|
tar_spi1, _ = _spi1_for_tar(preset, tar, spi1_base,
|
||||||
prefer_msl=True)
|
prefer_msl=True)
|
||||||
except ValueError as e:
|
except ValueError as exc:
|
||||||
err = {'success': False, 'error': str(e)}
|
err = {'success': False, 'error': str(exc)}
|
||||||
self._send_json(err, 400)
|
self._send_json(err, 400)
|
||||||
self._log_resp(err)
|
self._log_resp(err)
|
||||||
return
|
return
|
||||||
@@ -10532,31 +10652,67 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
self._send_json(err, 400)
|
self._send_json(err, 400)
|
||||||
self._log_resp(err)
|
self._log_resp(err)
|
||||||
return
|
return
|
||||||
resolved.append((tar, label, tar_spi1))
|
row_kvn = e.get('keyset')
|
||||||
|
row_ks = None
|
||||||
|
if row_kvn:
|
||||||
|
row_ks = _preset_keyset_by_kvn(preset, row_kvn)
|
||||||
|
if row_ks is None:
|
||||||
|
err = {'success': False,
|
||||||
|
'error': 'keyset %02X (TAR %s) is not defined in '
|
||||||
|
'the preset' % (row_kvn, tar)}
|
||||||
|
self._send_json(err, 400)
|
||||||
|
self._log_resp(err)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
msl = str(e.get('msl') or '')
|
||||||
|
if msl and msl != '00':
|
||||||
|
err = {'success': False,
|
||||||
|
'error': 'TAR %s needs a default keyset (its MSL '
|
||||||
|
'%s requires security) - set it in the '
|
||||||
|
'Cards tab' % (tar, msl)}
|
||||||
|
self._send_json(err, 400)
|
||||||
|
self._log_resp(err)
|
||||||
|
return
|
||||||
|
label = (str(e.get('desc') or '').strip()
|
||||||
|
or role_label.get(e.get('role') or '', ''))
|
||||||
|
resolved.append((tar, label, tar_spi1, row_kvn, row_ks))
|
||||||
|
explicit_cntr = str(body.get('cntr') or '').strip().upper()
|
||||||
state = {'steps': [], 'encode_error': None, 'failure': {},
|
state = {'steps': [], 'encode_error': None, 'failure': {},
|
||||||
# the counter: explicit `cntr` wins, else the preset's
|
'cntr': '00000000', 'preset_id': body.get('preset_id'),
|
||||||
# value for this keyset (the server owns the counters,
|
'kvn': None}
|
||||||
# v3.20.0)
|
sp = {'spi1': spi1_base, 'spi2': spi2, 'kic': '00', 'kid': '00',
|
||||||
'cntr': _preset_counter_seed(self.server, body, kvn) or '00000000',
|
'tar': '000000', 'kic_key': '', 'kid_key': '',
|
||||||
'preset_id': body.get('preset_id'), 'kvn': kvn}
|
|
||||||
sp = {'spi1': spi1_base, 'spi2': spi2, 'kic': kic, 'kid': kid, 'tar': '000000',
|
|
||||||
'kic_key': body.get('kicKey', ''), 'kid_key': body.get('kidKey', ''),
|
|
||||||
'include_cpi': body.get('includeCpi', True)}
|
'include_cpi': body.get('includeCpi', True)}
|
||||||
sys.stderr.write('TAR-PROBE: %d TAR(s), APDU %s, SPI %s/%s, kvn=%s\n'
|
sys.stderr.write('TAR-PROBE: %d TAR(s), APDU %s, SPI %s/%s\n'
|
||||||
% (len(entries), apdu, spi1_base or '-', spi2, kvn))
|
% (len(entries), apdu, spi1_base or '-', spi2))
|
||||||
_ram_progress_begin('tar-probe', len(entries))
|
_ram_progress_begin('tar-probe', len(entries))
|
||||||
try:
|
try:
|
||||||
for index, (tar, _label, tar_spi1) in enumerate(resolved):
|
for index, (tar, _label, tar_spi1, row_kvn, row_ks) in enumerate(resolved):
|
||||||
_ram_progress_step(index, 'TAR ' + tar)
|
_ram_progress_step(index, 'TAR ' + tar)
|
||||||
sp['tar'] = tar
|
sp['tar'] = tar
|
||||||
sp['spi1'] = tar_spi1
|
sp['spi1'] = tar_spi1
|
||||||
|
sp['kic'] = str((row_ks or {}).get('kic') or '00')
|
||||||
|
sp['kid'] = str((row_ks or {}).get('kid') or '00')
|
||||||
|
sp['kic_key'] = str((row_ks or {}).get('kicKey') or '')
|
||||||
|
sp['kid_key'] = str((row_ks or {}).get('kidKey') or '')
|
||||||
|
state['kvn'] = row_kvn
|
||||||
|
# Each keyset has its own counter (TS 102 225 A.1): seed
|
||||||
|
# the row's from the store (an explicit `cntr` override
|
||||||
|
# applies to the first row only).
|
||||||
|
if index == 0 and explicit_cntr:
|
||||||
|
state['cntr'] = explicit_cntr
|
||||||
|
else:
|
||||||
|
state['cntr'] = (_preset_counter_seed(
|
||||||
|
self.server, {'preset_id': body.get('preset_id')},
|
||||||
|
row_kvn) or '00000000')
|
||||||
_ram_send_gp_apdu(self.server, scc, sp, state, 'TAR ' + tar, apdu)
|
_ram_send_gp_apdu(self.server, scc, sp, state, 'TAR ' + tar, apdu)
|
||||||
finally:
|
finally:
|
||||||
_ram_progress_end()
|
_ram_progress_end()
|
||||||
results = []
|
results = []
|
||||||
for (tar, label, tar_spi1), step in zip(resolved, state['steps']):
|
for (tar, label, tar_spi1, row_kvn, _ks), step in zip(resolved, state['steps']):
|
||||||
results.append({
|
results.append({
|
||||||
'tar': tar, 'label': label, 'spi1': tar_spi1,
|
'tar': tar, 'label': label, 'spi1': tar_spi1,
|
||||||
|
'kvn': row_kvn,
|
||||||
'verdict': _tar_probe_verdict(step),
|
'verdict': _tar_probe_verdict(step),
|
||||||
'sw': step.get('sw'), 'por_status': step.get('por_status'),
|
'sw': step.get('sw'), 'por_status': step.get('por_status'),
|
||||||
'por_sw': step.get('por_sw'), 'por_data': step.get('por_data'),
|
'por_sw': step.get('por_sw'), 'por_data': step.get('por_data'),
|
||||||
@@ -10565,7 +10721,8 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
registered = sum(1 for r in results if r['verdict'] == 'registered')
|
registered = sum(1 for r in results if r['verdict'] == 'registered')
|
||||||
resp = {'success': True, 'results': results, 'steps': state['steps'],
|
resp = {'success': True, 'results': results, 'steps': state['steps'],
|
||||||
'registered': registered, 'total': len(results),
|
'registered': registered, 'total': len(results),
|
||||||
'final_cntr': state['cntr'], 'kvn': kvn,
|
'final_cntr': state['cntr'],
|
||||||
|
'kvn': resolved[0][3] if resolved else None,
|
||||||
'spi1': spi1_base, 'spi2': spi2, 'apdu': apdu}
|
'spi1': spi1_base, 'spi2': spi2, 'apdu': apdu}
|
||||||
self._send_json(resp)
|
self._send_json(resp)
|
||||||
self._log_resp(resp)
|
self._log_resp(resp)
|
||||||
|
|||||||
+91
-27
@@ -582,21 +582,6 @@ class CntrLowGuardHttpTests(unittest.TestCase):
|
|||||||
self.assertNotIn('suggested_cntr', resp)
|
self.assertNotIn('suggested_cntr', resp)
|
||||||
|
|
||||||
|
|
||||||
class TarProbeListTests(unittest.TestCase):
|
|
||||||
"""The PWA's TAR checklist must match the server's TAR_PROBE_TARS - the
|
|
||||||
panel is pre-populated client-side, the server's list is the API fallback,
|
|
||||||
and both must describe the same allocations (TS 101 220 Annex D)."""
|
|
||||||
|
|
||||||
def test_the_pwa_and_the_server_share_the_standard_tar_list(self):
|
|
||||||
from pysim_simple_server import server as srv
|
|
||||||
html = (ROOT / 'frontend' / 'index.html').read_text(encoding='utf-8')
|
|
||||||
block = html[html.index('const TAR_PROBE_TARS = ['):]
|
|
||||||
block = block[:block.index('\n];')]
|
|
||||||
pwa = re.findall(r"tar:\s*'([0-9A-F]{6})',\s*label:\s*'([^']*)'", block)
|
|
||||||
self.assertEqual(pwa, list(srv.TAR_PROBE_TARS))
|
|
||||||
self.assertTrue(pwa, 'the PWA list must not be empty')
|
|
||||||
|
|
||||||
|
|
||||||
class PresetStoreHttpTests(unittest.TestCase):
|
class PresetStoreHttpTests(unittest.TestCase):
|
||||||
"""The card preset endpoints (v3.8.0): the PWA's Cards tab talks to the
|
"""The card preset endpoints (v3.8.0): the PWA's Cards tab talks to the
|
||||||
server store instead of localStorage, and the counter an operation
|
server store instead of localStorage, and the counter an operation
|
||||||
@@ -655,7 +640,18 @@ class PresetStoreHttpTests(unittest.TestCase):
|
|||||||
|
|
||||||
def _preset(self, **kw):
|
def _preset(self, **kw):
|
||||||
p = {'name': 'C', 'keysets': [self._keyset()]}
|
p = {'name': 'C', 'keysets': [self._keyset()]}
|
||||||
|
if 'tars' not in kw:
|
||||||
|
# v3.26.0: the TARs name their default keyset
|
||||||
|
p['tars'] = [{'role': r, 'tar': t, 'msl': '16'}
|
||||||
|
for r, t in (('isd', '000000'), ('uiccRfm', 'B00000'),
|
||||||
|
('usimRfm', 'B00001'))]
|
||||||
p.update(kw)
|
p.update(kw)
|
||||||
|
if 'tars' not in kw and len(p.get('keysets') or []) > 1:
|
||||||
|
# a multi-keyset fixture must name its TAR keysets (a single-keyset
|
||||||
|
# one is auto-filled by the store): point them at the first keyset
|
||||||
|
kvn = int(str((p['keysets'][0] or {}).get('kic') or '10'), 16) >> 4
|
||||||
|
for t in p['tars']:
|
||||||
|
t['keyset'] = kvn
|
||||||
return p
|
return p
|
||||||
|
|
||||||
def test_the_get_route_is_card_free_and_reports_the_store(self):
|
def test_the_get_route_is_card_free_and_reports_the_store(self):
|
||||||
@@ -1136,6 +1132,55 @@ class PresetStoreHttpTests(unittest.TestCase):
|
|||||||
self.assertEqual(self.store.find_keyset(p['id'], 2)['cntr'], '0000000006')
|
self.assertEqual(self.store.find_keyset(p['id'], 2)['cntr'], '0000000006')
|
||||||
self.assertEqual(self.store.find_keyset(p['id'], 1)['cntr'], '0000000001')
|
self.assertEqual(self.store.find_keyset(p['id'], 1)['cntr'], '0000000001')
|
||||||
|
|
||||||
|
def test_an_operation_uses_the_tar_default_keyset(self):
|
||||||
|
# v3.26.0: without explicit KIc/KID the RAM-style APDU send resolves
|
||||||
|
# the keyset from the preset's TAR table (the ISD role entry)
|
||||||
|
p = self.store.add(self._preset(iccid='8970119000004600098', keysets=[
|
||||||
|
self._keyset(kic='25', kid='25', kicKey='CC' * 16, kidKey='DD' * 16,
|
||||||
|
cntr='0000000005')]))
|
||||||
|
built = []
|
||||||
|
patches = [
|
||||||
|
mock.patch.object(self.srv, '_build_secured_packet',
|
||||||
|
side_effect=lambda *a, **k: (built.append(a) or ('00' * 30, {}))),
|
||||||
|
mock.patch.object(self.srv, '_send_secured_packet', lambda *a, **k: {
|
||||||
|
'success': True, 'sw': '9000', 'bytes': 34, 'segments': 1,
|
||||||
|
'response_data': '027100000000'}),
|
||||||
|
mock.patch.object(self.srv, '_decode_por', lambda *a, **k: {
|
||||||
|
'response_status': 'por_ok', 'tar': '000000', 'cntr': '0000000005',
|
||||||
|
'pcntr': 0, 'rpl': 11, 'rhl': 10, 'raw': '',
|
||||||
|
'decoded': {'number_of_commands': 1, 'last_status_word': '9000',
|
||||||
|
'last_response_data': ''}}),
|
||||||
|
]
|
||||||
|
self.server.app = object()
|
||||||
|
for patch in patches:
|
||||||
|
patch.start()
|
||||||
|
try:
|
||||||
|
status, resp = self._post('/api/send-ota', {
|
||||||
|
'apdu': '00A40000023F00', 'spi2': '01', 'preset_id': p['id']})
|
||||||
|
finally:
|
||||||
|
for patch in reversed(patches):
|
||||||
|
patch.stop()
|
||||||
|
self.assertEqual(status, 200, resp)
|
||||||
|
args = built[0]
|
||||||
|
self.assertEqual((args[2], args[3]), ('25', '25')) # the TAR keyset's KIc/KID
|
||||||
|
self.assertEqual(args[5], '0000000005') # ... and its counter
|
||||||
|
self.assertEqual(resp['final_cntr'], '0000000006')
|
||||||
|
self.assertEqual(self.store.find_keyset(p['id'], 2)['cntr'], '0000000006')
|
||||||
|
|
||||||
|
def test_a_security_tar_without_a_keyset_refuses_the_operation(self):
|
||||||
|
# a legacy (v5) preset loads leniently; an operation addressed to a
|
||||||
|
# security TAR without a default keyset refuses instead of sending a
|
||||||
|
# keyless packet the card would reject (v3.26.0)
|
||||||
|
p = self.store.add(self._preset(keysets=[self._keyset(),
|
||||||
|
self._keyset(kic='29', kid='29')]))
|
||||||
|
# simulate the legacy file: the ISD TAR carries no default keyset
|
||||||
|
self.store._presets[0]['tars'][0]['keyset'] = None
|
||||||
|
self.server.app = object()
|
||||||
|
status, resp = self._post('/api/send-ota', {
|
||||||
|
'apdu': '00A40000023F00', 'spi2': '01', 'preset_id': p['id']})
|
||||||
|
self.assertEqual(status, 400, resp)
|
||||||
|
self.assertIn('needs a default keyset', resp['error'])
|
||||||
|
|
||||||
def test_a_ram_install_refuses_an_undefined_key_version(self):
|
def test_a_ram_install_refuses_an_undefined_key_version(self):
|
||||||
p = self.store.add(self._preset(iccid='8970119000004600098'))
|
p = self.store.add(self._preset(iccid='8970119000004600098'))
|
||||||
status, resp = self._post('/api/ram-install', {
|
status, resp = self._post('/api/ram-install', {
|
||||||
@@ -1200,8 +1245,15 @@ class PresetStoreHttpTests(unittest.TestCase):
|
|||||||
self.assertEqual(sent[0].lower(), '0d00010000b0000000000000010000a40000023f00')
|
self.assertEqual(sent[0].lower(), '0d00010000b0000000000000010000a40000023f00')
|
||||||
|
|
||||||
def test_the_tar_probe_classifies_each_tar(self):
|
def test_the_tar_probe_classifies_each_tar(self):
|
||||||
p = self.store.add(self._preset(iccid='8970119000004600098', keysets=[
|
# v3.26.0: the probe checks the preset's TAR list only and each row
|
||||||
self._keyset(kic='25', kid='25', kicKey='AA', kidKey='BB', cntr='0000000005')]))
|
# uses its TAR's default keyset (a single-keyset preset auto-fills it)
|
||||||
|
p = self.store.add(self._preset(iccid='8970119000004600098',
|
||||||
|
tars=[{'role': 'isd', 'tar': '000000', 'msl': '16'},
|
||||||
|
{'role': 'uiccRfm', 'tar': 'B00000', 'msl': '16'},
|
||||||
|
{'role': 'usimRfm', 'tar': 'B00001', 'msl': '16'},
|
||||||
|
{'tar': 'B00200', 'msl': '16', 'desc': 'Vendor'}],
|
||||||
|
keysets=[self._keyset(kic='25', kid='25', kicKey='AA', kidKey='BB',
|
||||||
|
cntr='0000000005')]))
|
||||||
seen = []
|
seen = []
|
||||||
|
|
||||||
def fake_step(server, scc, sp, state, name, apdu, silent=False):
|
def fake_step(server, scc, sp, state, name, apdu, silent=False):
|
||||||
@@ -1222,8 +1274,7 @@ class PresetStoreHttpTests(unittest.TestCase):
|
|||||||
self.server.app = object() # the probe only checks truthiness
|
self.server.app = object() # the probe only checks truthiness
|
||||||
with mock.patch.object(self.srv, '_ram_send_gp_apdu', fake_step):
|
with mock.patch.object(self.srv, '_ram_send_gp_apdu', fake_step):
|
||||||
status, resp = self._post('/api/tar-probe', {
|
status, resp = self._post('/api/tar-probe', {
|
||||||
'preset_id': p['id'], 'spi1': '16', 'kic': '25', 'kid': '25',
|
'preset_id': p['id'], 'spi1': '16',
|
||||||
'kicKey': 'AA', 'kidKey': 'BB', 'cntr': '0000000005',
|
|
||||||
'tars': ['000000', 'B00000', 'B00001', 'B00200']})
|
'tars': ['000000', 'B00000', 'B00001', 'B00200']})
|
||||||
self.assertEqual(status, 200, resp)
|
self.assertEqual(status, 200, resp)
|
||||||
self.assertEqual(seen, ['000000', 'B00000', 'B00001', 'B00200'])
|
self.assertEqual(seen, ['000000', 'B00000', 'B00001', 'B00200'])
|
||||||
@@ -1233,19 +1284,32 @@ class PresetStoreHttpTests(unittest.TestCase):
|
|||||||
self.assertEqual(resp['registered'], 2)
|
self.assertEqual(resp['registered'], 2)
|
||||||
self.assertEqual(resp['total'], 4)
|
self.assertEqual(resp['total'], 4)
|
||||||
self.assertEqual(resp['kvn'], 2)
|
self.assertEqual(resp['kvn'], 2)
|
||||||
self.assertEqual(resp['results'][0]['label'], 'Issuer Security Domain (compact)')
|
self.assertEqual([r['kvn'] for r in resp['results']], [2, 2, 2, 2])
|
||||||
|
self.assertEqual(resp['results'][0]['label'], 'ISD')
|
||||||
|
self.assertEqual(resp['results'][3]['label'], 'Vendor')
|
||||||
self.assertEqual(resp['results'][0]['por_sw'], '6D00')
|
self.assertEqual(resp['results'][0]['por_sw'], '6D00')
|
||||||
|
|
||||||
def test_the_tar_probe_needs_a_counter_check_and_a_defined_keyset(self):
|
def test_the_tar_probe_refuses_what_it_cannot_probe(self):
|
||||||
self.server.app = object()
|
self.server.app = object()
|
||||||
status, resp = self._post('/api/tar-probe', {'spi1': '00', 'kic': '15', 'kid': '15'})
|
# a preset is required - the probe checks its TAR list
|
||||||
|
status, resp = self._post('/api/tar-probe', {'spi1': '16'})
|
||||||
|
self.assertEqual(status, 400, resp)
|
||||||
|
self.assertIn('preset_id', resp['error'])
|
||||||
|
p = self.store.add(self._preset(iccid='8970119000004600098'))
|
||||||
|
# a TAR outside the preset is refused (no arbitrary TARs)
|
||||||
|
status, resp = self._post('/api/tar-probe',
|
||||||
|
{'preset_id': p['id'], 'spi1': '16',
|
||||||
|
'tars': ['B0FFFF']})
|
||||||
|
self.assertEqual(status, 400, resp)
|
||||||
|
self.assertIn('not in the preset', resp['error'])
|
||||||
|
# an MSL 00 TAR has no counter check: the probe refuses it
|
||||||
|
p00 = self.store.add(self._preset(iccid='8970119000004600010',
|
||||||
|
tars=[{'role': 'isd', 'tar': '000000', 'msl': '00'},
|
||||||
|
{'role': 'uiccRfm', 'tar': 'B00000', 'msl': '16'},
|
||||||
|
{'role': 'usimRfm', 'tar': 'B00001', 'msl': '16'}]))
|
||||||
|
status, resp = self._post('/api/tar-probe', {'preset_id': p00['id']})
|
||||||
self.assertEqual(status, 400, resp)
|
self.assertEqual(status, 400, resp)
|
||||||
self.assertIn('counter check', resp['error'])
|
self.assertIn('counter check', resp['error'])
|
||||||
p = self.store.add(self._preset(iccid='8970119000004600098'))
|
|
||||||
status, resp = self._post('/api/tar-probe',
|
|
||||||
{'preset_id': p['id'], 'kic': '35', 'kid': '35'})
|
|
||||||
self.assertEqual(status, 400, resp)
|
|
||||||
self.assertIn('not defined', resp['error'])
|
|
||||||
|
|
||||||
def test_a_store_write_failure_answers_a_json_500(self):
|
def test_a_store_write_failure_answers_a_json_500(self):
|
||||||
with mock.patch.object(self.store, 'add', side_effect=OSError('read-only file system')):
|
with mock.patch.object(self.store, 'add', side_effect=OSError('read-only file system')):
|
||||||
|
|||||||
+70
-2
@@ -28,8 +28,20 @@ def keyset(**kw):
|
|||||||
return ks
|
return ks
|
||||||
|
|
||||||
|
|
||||||
|
# The three role entries with their default keyset (v3.26.0) - the base for
|
||||||
|
# the multi-keyset fixtures, where no auto-fill happens.
|
||||||
|
ROLE_TARS = [{'role': role, 'tar': tar, 'msl': '16', 'keyset': 1}
|
||||||
|
for role, tar in (('isd', '000000'), ('uiccRfm', 'B00000'),
|
||||||
|
('usimRfm', 'B00001'))]
|
||||||
|
|
||||||
|
|
||||||
def preset(**kw):
|
def preset(**kw):
|
||||||
p = {'name': 'C', 'keysets': [keyset()]}
|
p = {'name': 'C', 'keysets': [keyset()]}
|
||||||
|
if 'tars' not in kw:
|
||||||
|
# v3.26.0: the TARs carry their default keyset - the fixtures name
|
||||||
|
# keyset 1, so a multi-keyset preset still validates (a single-keyset
|
||||||
|
# one would be auto-filled anyway).
|
||||||
|
p['tars'] = [dict(t) for t in ROLE_TARS]
|
||||||
p.update(kw)
|
p.update(kw)
|
||||||
return p
|
return p
|
||||||
|
|
||||||
@@ -174,6 +186,54 @@ class StoreCrudTests(unittest.TestCase):
|
|||||||
self.assertEqual(self.store.find_keyset(p['id'], 2)['cntr'], '0000000005')
|
self.assertEqual(self.store.find_keyset(p['id'], 2)['cntr'], '0000000005')
|
||||||
self.assertIsNone(self.store.find_keyset(p['id'], 3))
|
self.assertIsNone(self.store.find_keyset(p['id'], 3))
|
||||||
|
|
||||||
|
def test_a_tar_keyset_is_mandatory_for_a_nonzero_msl(self):
|
||||||
|
# v3.26.0: the counters are keyset-linked, so any non-zero MSL needs a
|
||||||
|
# default keyset; MSL 00 may omit it (the packet is built keyless) and
|
||||||
|
# may still carry one (some cards reject the '00/00' KIc/KID form)
|
||||||
|
two = [keyset(), keyset(kic='29', kid='29')]
|
||||||
|
with self.assertRaises(presets.PresetError) as ctx:
|
||||||
|
self.store.add(preset(keysets=two, tars=ROLE_TARS + [
|
||||||
|
{'tar': 'B00011', 'msl': '16', 'desc': 'push'}]))
|
||||||
|
self.assertIn('needs a default keyset', str(ctx.exception))
|
||||||
|
p = self.store.add(preset(keysets=two, tars=ROLE_TARS + [
|
||||||
|
{'tar': 'B00011', 'msl': '00', 'desc': 'push'}]))
|
||||||
|
self.assertIsNone(p['tars'][3]['keyset'])
|
||||||
|
p = self.store.add(preset(keysets=two, tars=ROLE_TARS + [
|
||||||
|
{'tar': 'B00011', 'msl': '00', 'keyset': 1}]))
|
||||||
|
self.assertEqual(p['tars'][3]['keyset'], 1)
|
||||||
|
|
||||||
|
def test_a_tar_keyset_must_exist_in_the_preset(self):
|
||||||
|
with self.assertRaises(presets.PresetError) as ctx:
|
||||||
|
self.store.add(preset(keysets=[keyset(), keyset(kic='29', kid='29')],
|
||||||
|
tars=ROLE_TARS + [
|
||||||
|
{'tar': 'B00011', 'msl': '16', 'keyset': 3}]))
|
||||||
|
self.assertIn('does not define', str(ctx.exception))
|
||||||
|
|
||||||
|
def test_a_single_keyset_fills_the_tar_defaults(self):
|
||||||
|
# v3.26.0: one keyset in the preset sets every TAR's default keyset
|
||||||
|
p = self.store.add(preset(tars=[{'tar': 'B00011', 'msl': '16'},
|
||||||
|
{'tar': 'B00012', 'msl': '00'}]))
|
||||||
|
self.assertTrue(all(t['keyset'] == 1 for t in p['tars']))
|
||||||
|
# several keysets leave an MSL 00 entry alone...
|
||||||
|
p = self.store.add(preset(keysets=[keyset(), keyset(kic='29', kid='29')],
|
||||||
|
tars=ROLE_TARS + [
|
||||||
|
{'tar': 'B00011', 'msl': '00'}]))
|
||||||
|
self.assertIsNone(p['tars'][3]['keyset'])
|
||||||
|
# ...and a non-zero MSL must name one explicitly
|
||||||
|
p = self.store.add(preset(keysets=[keyset(), keyset(kic='29', kid='29')],
|
||||||
|
tars=ROLE_TARS + [
|
||||||
|
{'tar': 'B00011', 'msl': '16', 'keyset': 2}]))
|
||||||
|
self.assertEqual(p['tars'][3]['keyset'], 2)
|
||||||
|
|
||||||
|
def test_tar_keyset_string_forms_and_the_helper(self):
|
||||||
|
p = self.store.add(preset(keysets=[keyset(), keyset(kic='29', kid='29')],
|
||||||
|
tars=ROLE_TARS + [
|
||||||
|
{'tar': 'B00011', 'msl': '16', 'keyset': '02'},
|
||||||
|
{'tar': 'B00012', 'msl': '16', 'keyset': '2'}]))
|
||||||
|
self.assertEqual([t['keyset'] for t in p['tars']], [1, 1, 1, 2, 2])
|
||||||
|
self.assertEqual(presets.tar_keyset(p, 'B00011'), 2)
|
||||||
|
self.assertIsNone(presets.tar_keyset(p, 'B00099'))
|
||||||
|
|
||||||
def test_validation_requires_at_least_one_keyset(self):
|
def test_validation_requires_at_least_one_keyset(self):
|
||||||
with self.assertRaises(presets.PresetError) as ctx:
|
with self.assertRaises(presets.PresetError) as ctx:
|
||||||
self.store.add(preset(keysets=[]))
|
self.store.add(preset(keysets=[]))
|
||||||
@@ -271,11 +331,19 @@ class StoreCrudTests(unittest.TestCase):
|
|||||||
self.assertEqual(up['id'], p['id'])
|
self.assertEqual(up['id'], p['id'])
|
||||||
self.assertEqual(up['name'], 'Renamed')
|
self.assertEqual(up['name'], 'Renamed')
|
||||||
self.assertEqual(up['keysets'][0]['kic'], '15')
|
self.assertEqual(up['keysets'][0]['kic'], '15')
|
||||||
# the keysets are replaceable as a whole (the Cards tab edit path)
|
# replacing the keysets with another number must re-point the TARs'
|
||||||
up = self.store.update(p['id'], {'keysets': [keyset(kic='29', kid='29',
|
# default keysets in the same update (the Cards-form edit path)
|
||||||
|
with self.assertRaises(presets.PresetError) as ctx:
|
||||||
|
self.store.update(p['id'], {'keysets': [keyset(kic='29', kid='29',
|
||||||
cntr='0000000009')]})
|
cntr='0000000009')]})
|
||||||
|
self.assertIn('does not define', str(ctx.exception))
|
||||||
|
tars = [dict(t, keyset=2) for t in up['tars']]
|
||||||
|
up = self.store.update(p['id'], {'keysets': [keyset(kic='29', kid='29',
|
||||||
|
cntr='0000000009')],
|
||||||
|
'tars': tars})
|
||||||
self.assertEqual(presets.keyset_kvn(up['keysets'][0]), 2)
|
self.assertEqual(presets.keyset_kvn(up['keysets'][0]), 2)
|
||||||
self.assertEqual(up['keysets'][0]['cntr'], '0000000009')
|
self.assertEqual(up['keysets'][0]['cntr'], '0000000009')
|
||||||
|
self.assertTrue(all(t['keyset'] == 2 for t in up['tars']))
|
||||||
self.assertIsNone(self.store.update('nope', {'name': 'x'}))
|
self.assertIsNone(self.store.update('nope', {'name': 'x'}))
|
||||||
|
|
||||||
def test_the_counter_must_be_hex_and_at_most_ten_digits(self):
|
def test_the_counter_must_be_hex_and_at_most_ten_digits(self):
|
||||||
|
|||||||
@@ -25,11 +25,12 @@ def _resolver(name):
|
|||||||
return {n.upper(): c for c, n in S.PROACTIVE_TYPE_NAMES.items()}.get(name.upper())
|
return {n.upper(): c for c, n in S.PROACTIVE_TYPE_NAMES.items()}.get(name.upper())
|
||||||
|
|
||||||
|
|
||||||
def preset_tars(isd='000000', msl='16'):
|
def preset_tars(isd='000000', msl='16', keyset=1):
|
||||||
"""The three mandatory role entries of a card preset's TAR table."""
|
"""The three mandatory role entries of a card preset's TAR table (v3.26.0:
|
||||||
return [{'role': 'isd', 'tar': isd, 'msl': msl, 'desc': ''},
|
each carries its default keyset - the fixtures name keyset 1)."""
|
||||||
{'role': 'uiccRfm', 'tar': 'B00000', 'msl': '16', 'desc': ''},
|
return [{'role': 'isd', 'tar': isd, 'msl': msl, 'desc': '', 'keyset': keyset},
|
||||||
{'role': 'usimRfm', 'tar': 'B00001', 'msl': '16', 'desc': ''}]
|
{'role': 'uiccRfm', 'tar': 'B00000', 'msl': '16', 'desc': '', 'keyset': keyset},
|
||||||
|
{'role': 'usimRfm', 'tar': 'B00001', 'msl': '16', 'desc': '', 'keyset': keyset}]
|
||||||
|
|
||||||
|
|
||||||
class TestValidation(unittest.TestCase):
|
class TestValidation(unittest.TestCase):
|
||||||
@@ -1974,7 +1975,8 @@ class TestSuiteRunner(SuiteRunnerTestCase):
|
|||||||
preset = {'id': 'p1',
|
preset = {'id': 'p1',
|
||||||
'keysets': [{'kic': '15', 'kid': '15', 'kicKey': '00' * 16,
|
'keysets': [{'kic': '15', 'kid': '15', 'kicKey': '00' * 16,
|
||||||
'kidKey': '00' * 16, 'cntr': '0000000010'}],
|
'kidKey': '00' * 16, 'cntr': '0000000010'}],
|
||||||
'tars': [{'role': 'isd', 'tar': 'B00000', 'msl': '16'}]}
|
'tars': [{'role': 'isd', 'tar': 'B00000', 'msl': '16',
|
||||||
|
'keyset': 1}]}
|
||||||
members = [({'script_id': 'a' * 32, 'role': 'member', 'on_fail': 'stop'}, script),
|
members = [({'script_id': 'a' * 32, 'role': 'member', 'on_fail': 'stop'}, script),
|
||||||
({'script_id': 'b' * 32, 'role': 'member', 'on_fail': 'stop'}, script)]
|
({'script_id': 'b' * 32, 'role': 'member', 'on_fail': 'stop'}, script)]
|
||||||
with mock.patch.object(S, '_build_secured_packet',
|
with mock.patch.object(S, '_build_secured_packet',
|
||||||
|
|||||||
Reference in New Issue
Block a user