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:
2026-10-14 12:21:16 +03:00
parent 6e4494195c
commit cb40662e4e
18 changed files with 955 additions and 611 deletions
File diff suppressed because one or more lines are too long
+6 -6
View File
@@ -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>
<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 &sect;8.2.1.3.2.4 &mdash; 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 &mdash; 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 &mdash; 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 &sect;8.2.1.3.2.4 &mdash; 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 &mdash; 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 &mdash; 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>
<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>
@@ -314,7 +314,7 @@
<p class="text-sm mb-3">A &ldquo;Verify vs pySim&rdquo; button cross-checks the assembled packet against pySim&rsquo;s reference <code class="font-mono text-sm">OtaDialectSms.encode_cmd</code>. A &ldquo;Send to Card&rdquo; 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&rsquo;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 &sect;4.3) &mdash; 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&rsquo;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&rsquo;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 &mdash; b5 b4 &ne; 00; without one no counter is advanced or saved) and clears the packet &mdash; the server persists the accepted counter into the preset, the form&rsquo;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&rsquo;s CNTR field is a copy of the packet&rsquo;s), so the notice offers a bounded <strong>Sync counter</strong> probe &mdash; 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) &mdash; 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>
<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 &sect;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&rsquo;s form, Le=&rsquo;00&rsquo; returns the listing directly), and a registry query the card refuses in the current format is retried in the other formats &mdash; 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 &mdash; 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) &mdash; some cards serve the expanded next page only wrapped in the <code class="font-mono text-sm">AA</code> template &mdash; 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&rsquo;s inferred <strong>ETSI release</strong> (<code class="font-mono text-sm">uicc.toolkit</code> version &rarr; 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 &mdash; the package&rsquo;s module AIDs are the applet instance AIDs, and the row is annotated as coming from the app instance &mdash; 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> &mdash; the keyset selector is gone &mdash; 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 &sect;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&rsquo;s form, Le=&rsquo;00&rsquo; returns the listing directly), and a registry query the card refuses in the current format is retried in the other formats &mdash; 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 &mdash; 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) &mdash; some cards serve the expanded next page only wrapped in the <code class="font-mono text-sm">AA</code> template &mdash; 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&rsquo;s inferred <strong>ETSI release</strong> (<code class="font-mono text-sm">uicc.toolkit</code> version &rarr; 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 &mdash; the package&rsquo;s module AIDs are the applet instance AIDs, and the row is annotated as coming from the app instance &mdash; 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>
<table class="w-full text-sm mb-3 border-collapse">
@@ -342,13 +342,13 @@
</section>
<section class="mb-10">
<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> &mdash; the standard allocations of TS 101 220 V18.3.0 Annex D, Table D.1, each row carrying the document it comes from &mdash; with a harmless C-APDU (default <code class="font-mono text-sm">SELECT MF</code>) using the view's preset and keyset &mdash; 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 &ne; 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 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 &mdash; losing them is harmless.</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 &mdash; 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 &ne; 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 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">Each packet the card accepts <strong>consumes a counter</strong>, which is saved into the preset's keyset &mdash; 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 &mdash; 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>
<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 &mdash; a closed tab or a second browser window can no longer lose an increment. A preset carries <strong>several keysets</strong> &mdash; a keyset's number is the <code class="font-mono text-sm">b8..b5</code> nibble of its KIc/KID (TS 102 225 &sect;5.1.2/A.2): KIc and KID of a keyset must carry the same number (01&ndash;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 &mdash; 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) &mdash; <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 &mdash; or the selected preset belongs to another card &mdash; 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 &mdash; PIN1/PUK1/PIN2/PUK2 as 4&ndash;8 decimal digits and the ADM key &mdash; 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> &mdash; 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 &mdash; a closed tab or a second browser window can no longer lose an increment. A preset carries <strong>several keysets</strong> &mdash; a keyset's number is the <code class="font-mono text-sm">b8..b5</code> nibble of its KIc/KID (TS 102 225 &sect;5.1.2/A.2): KIc and KID of a keyset must carry the same number (01&ndash;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 &mdash; 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) &mdash; <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 &mdash; or the selected preset belongs to another card &mdash; 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 &mdash; PIN1/PUK1/PIN2/PUK2 as 4&ndash;8 decimal digits and the ADM key &mdash; stored for later use), <strong>TAR</strong> (the TAR table &mdash; 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 &mdash; the counters are keyset-linked &mdash; 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> &mdash; 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">
<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>
+255 -284
View File
@@ -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">
<option value="" data-l10n="— Select card —">— Select card —</option>
</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 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-1">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">TAR (3 bytes)</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">
<datalist id="sp-tar-list"></datalist>
<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>
<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>
</div>
<div class="flex-1">
<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>
</select>
</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">
<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">
@@ -934,26 +927,17 @@
<option value="" data-l10n="— Select card —">— Select card —</option>
</select>
</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 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">
<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">
<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>
</span>
</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>
<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>
@@ -1719,6 +1703,7 @@
<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-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="w-12 border-b border-gray-200 dark:border-slate-700"></th>
</tr></thead>
@@ -1907,7 +1892,7 @@
// ===== Version =====
// Single source of truth for the PWA version: shown in the header and used
// 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;
// ===== view history (the browser Back/Forward buttons) =====
@@ -5032,20 +5017,71 @@ function spPresetTar(role) {
return cardsRoleTar(c, role);
}
// The TAR field's suggestions: the preset's TAR table, each entry labelled
// with its description (or its role) and MSL - free text stays possible.
function spTarListRender(preset) {
const list = document.getElementById('sp-tar-list');
if (!list) return;
// The SCP80 TAR selector (v3.26.0): the preset's TAR table - the role entries
// labelled by their role, the free rows by their description - each with its
// MSL and default keyset. A change applies the TAR's keyset to the form and
// seeds the SPI1 from its MSL.
function spTarOptionsHtml(preset) {
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 text = label + (e.msl ? (label ? ' ' : '') + '(MSL ' + e.msl + ')' : '');
return '<option value="' + esc(e.tar || '') + '"' +
(text ? ' label="' + esc(text) + '"' : '') + '></option>';
if (label) bits.push(label);
if (e.msl) bits.push('MSL ' + e.msl);
if (e.keyset) bits.push(t('Keyset') + ' ' + e.keyset);
return '<option value="' + esc(e.tar || '') + '">' + esc(bits.join(' \u00b7 ')) + '</option>';
}).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) {
const val = document.getElementById(sourceId).value;
if (!val) return;
@@ -5055,7 +5091,10 @@ function packToSp(sourceId) {
const key = spTarKeyForPack(sourceId);
if (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;
}
@@ -10436,8 +10475,10 @@ function scriptsCapFileChanged() {
// Collect SP params from the SP form (populated by ramApplyCard -> cardsApply)
function getRamSpParams(selId) {
// `preset_id` names the server-side preset the operation belongs to: the
// server persists the counter it consumes there (v3.8.0). `selId` picks
// the view's preset selector (RAM by default, the TAR probe its own).
// server persists the counter it consumes there (v3.8.0) and resolves the
// 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 idx = sel ? parseInt(sel.value, 10) : NaN;
const preset = (!isNaN(idx) && cards[idx]) ? cards[idx] : null;
@@ -10445,11 +10486,7 @@ function getRamSpParams(selId) {
preset_id: (preset && preset.id) || '',
spi1: document.getElementById('sp-spi1').value,
spi2: document.getElementById('sp-spi2-hex').value,
kic: document.getElementById('sp-kic-hex').value,
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, ''),
tar: preset ? cardsRoleTar(preset, 'isd') : '',
ram_format: (document.getElementById('ram-format') || {}).value || 'auto',
};
// Override only when typed; empty = the server seeds the counter from the
@@ -11928,10 +11965,8 @@ async function ramDeleteFromExplorer(aid, withCascade) {
const ksErr = ramKeysetGuard();
if (ksErr) { alert(ksErr); return; }
const sp = getRamSpParams();
if (!sp.kicKey || !sp.kidKey) {
alert(t('Select a card preset with keys first (RAM subtab → Card preset)'));
return;
}
// v3.26.0: the keyset fields no longer travel - the server resolves the
// keyset (and its keys) from the preset's ISD TAR entry
const label = withCascade ? t('Delete') + ' (' + t('cascade') + ')' : t('Delete');
if (!confirm(label + ' — ' + t('AID:') + ' ' + aid + '?')) return;
const apdu = ramDeleteApdu(aid, withCascade);
@@ -12494,59 +12529,27 @@ async function ramInstallApp(sp, op) {
}
// ===== TAR probe (v3.8.0) =====
// One secured packet per TAR from the preset's keyset, classified by the
// card's ENVELOPE verdict and the application's PoR. The standard TAR list
// lives on the server (TS 101 220 V18.3.0 Annex D, Table D.1) and is used when
// the list field is empty. Each packet the card accepts consumes a counter,
// which the server persists into the keyset (one per accepted TAR).
// One secured packet per TAR of the selected preset, classified by the card's
// ENVELOPE verdict and the application's PoR. v3.26.0: the probe checks the
// preset's TAR list only - each row uses its TAR's MSL and default keyset, and
// every packet the card accepts consumes a counter, which the server persists
// 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 document each allocation comes from. Kept in sync with the server's
// TAR_PROBE_TARS by a test; custom TARs live in localStorage (best effort).
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.
// The probe's rows: the selected preset's TAR list (the role entries labelled
// by their role, the free rows by their description; the tooltip carries the
// MSL and the default keyset). There is no custom-TAR input anymore.
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
// document in the tooltip and a remove button on custom rows.
// The checklist rows (pure): a checkbox per TAR, checked by default.
function tarProbeRowsHtml(rows) {
return (rows || []).map(r => {
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>' +
'<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>' +
(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>';
}).join('');
}
@@ -12570,34 +12570,20 @@ function tarProbeChecked() {
function tarProbeRender() {
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) {
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) {
return { registered: 'registered', no_answer: 'accepted, no answer',
no_por: 'accepted, no PoR', refused: 'refused' }[verdict] || verdict;
@@ -12694,46 +12680,29 @@ function tarPresetIdx() {
}
function tarApplyCard(idx) {
// copy the selected preset into the shared SP form fields (the probe reads
// kic/kid/keys/counter from there, like the RAM view does)
// v3.26.0: the probe reads its rows from the selected preset (each TAR
// 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);
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);
tarProbeRender();
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() {
const tars = tarProbeChecked();
if (!tars.length) { alert(t('Select at least one TAR')); return; }
const cardIdx = parseInt(document.getElementById('tar-card-sel').value, 10);
if (!isNaN(cardIdx) && cards[cardIdx]) _ramCardIdx = cardIdx;
spRefreshFromPreset('tar-card-sel');
const ksErr = tarKeysetGuard();
if (ksErr) { alert(ksErr); return; }
const sp = getRamSpParams('tar-card-sel');
if (!sp.kicKey || !sp.kidKey) {
alert(t('Select a card preset with keys first (RAM subtab → Card preset)'));
return;
}
await tarProbe(sp, tars);
const idx = tarPresetIdx();
const preset = idx >= 0 ? cards[idx] : null;
if (!preset) { alert(t('Select a card preset')); return; }
// v3.26.0: the probe checks the preset's TARs and resolves each row's
// keyset server-side; only the preset and the (fallback) SPI1 travel.
const spi1 = (document.getElementById('sp-spi1') || {}).value || '';
await tarProbe({ preset_id: preset.id, spi1: spi1 }, tars);
}
// 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 box = ramCntrLowBox(view === 'sp' ? 'sp' : 'ram');
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 body = { preset_id: preset.id };
const kvn = kvnOf(own) || kvnOf(other);
if (kvn) body.kvn = kvn;
const tar = val('sp-tar').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
// v3.26.0: the keyset follows the operation's TAR - the RAM operations are
// addressed to the ISD role entry, a plain send to its selected TAR (the
// 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 (val('sp-spi1')) body.spi1 = val('sp-spi1');
// 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.
spRefreshFromPreset('ram-card-sel');
const sp = getRamSpParams();
if (!sp.kicKey || !sp.kidKey) {
alert(t('Select a card preset with keys first (RAM subtab → Card preset)'));
return;
}
// v3.26.0: the keyset fields no longer travel - the server resolves the
// keyset (and its keys) from the preset's ISD TAR entry
try {
if (op === 'explore') await ramExplore(sp);
else if (op === 'install-cap') await ramInstallCap(sp);
@@ -13543,28 +13503,6 @@ function spKeysetCheck(preset, kicHex, kidHex) {
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) =====
let _cardsKeysetCount = 0;
@@ -13595,12 +13533,20 @@ function cardsKeysetRowsRender(keysets) {
function cardsKeysetAdd() {
cardsKeysetRowsRender(cardsKeysetsFromForm().concat([
{ kic: '', kid: '', kicKey: '', kidKey: '', cntr: '' }]));
cardsTarKeysetRefresh();
}
function cardsKeysetRemove(i) {
const rows = cardsKeysetsFromForm();
rows.splice(i, 1);
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) {
@@ -13614,6 +13560,9 @@ function cardsKeysetKvnUpdate(i) {
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)')
: 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).
@@ -13648,11 +13597,42 @@ const CARDS_TAR_ROLE_DESC = {
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) {
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] || '';
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) {
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>';
@@ -13673,7 +13653,7 @@ function cardsTarRowsRender(entries) {
const list = entries || [];
const roles = CARDS_TAR_ROLES.map(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);
_cardsTarCount = free.length;
@@ -13682,7 +13662,7 @@ function cardsTarRowsRender(entries) {
}
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.
@@ -13703,13 +13683,21 @@ function cardsFreeTarsFromForm() {
return el ? String(el.value).trim() : '';
};
const e = { tar: get('tar').replace(/\s/g, '').toUpperCase(),
msl: get('msl').replace(/\s/g, '').toUpperCase(), desc: get('desc') };
if (!e.tar && !e.msl && !e.desc) continue;
msl: get('msl').replace(/\s/g, '').toUpperCase(), desc: get('desc'),
keyset: cardsTarKeysetFromForm(idx) };
if (!e.tar && !e.msl && !e.desc && !e.keyset) continue;
out.push(e);
}
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
// stays empty) plus the free rows - the re-render source for Add/Remove, so an
// emptied field is not silently refilled with its default.
@@ -13721,7 +13709,8 @@ function cardsTarsFromTable() {
};
return { role: role,
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());
}
@@ -13732,45 +13721,24 @@ function cardsTarsFromTable() {
function cardsTarsFromForm() {
return cardsTarsFromTable().map(e => 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);
}
// The role rows back to their pre-settled defaults (TAR + MSL 16) and no free
// rows.
// The role rows back to their pre-settled defaults (TAR + MSL 16, no keyset)
// and no free rows.
function cardsTarsReset() {
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) =====
// The selector follows the KIc/KID bytes: a keyset of the selected preset that
// matches their keyset number is selected; a number the preset does not define
// shows as "not defined" (and the packet actions refuse it).
// ===== Keyset resolution (SCP80 / RAM / TAR probe) =====
// v3.26.0: the keyset selectors are gone - each TAR carries its default keyset
// in the preset, the SCP80 TAR select applies it to the form and the RAM
// operations use the ISD role entry (the server resolves the same rule). Only
// the start buttons' availability refresh remains here.
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();
}
@@ -13797,26 +13765,6 @@ function spKeysetApply(ks) {
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
// 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
@@ -13869,30 +13817,64 @@ function ramPresetIdx() {
// '' 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
// 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() {
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 kid = (document.getElementById('sp-kid-hex') || {}).value;
return spKeysetCheck(idx >= 0 ? cards[idx] : null, kic, kid);
return spKeysetCheck(preset, kic, kid);
}
function ramKeysetGuard() {
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 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
// form's keyset number one of that preset's keysets (the click-time guards keep
// the byte-level checks). '00'/'00' (no security) with a preset selected stays
// allowed - it is a deliberate form choice.
// operation TAR's keyset usable (the click-time guards keep the byte-level
// checks). '00'/'00' (no security) with a preset selected stays allowed - it
// is a deliberate form choice.
function spKeysetStartReady(view) {
const idx = view === 'ram' ? ramPresetIdx() : (view === 'tar' ? tarPresetIdx() : spPresetIdx());
if (idx < 0) return false;
const kic = (document.getElementById('sp-kic-hex') || {}).value;
const kid = (document.getElementById('sp-kid-hex') || {}).value;
return spKeysetCheck(cards[idx], kic, kid) === '';
if (view === 'ram') {
if (ramPresetIdx() < 0) return false;
return ramKeysetGuard() === '';
}
if (view === 'tar') return tarPresetIdx() >= 0;
if (spPresetIdx() < 0) return false;
return spKeysetGuard() === '';
}
function cardsFormValues() {
@@ -14257,40 +14239,22 @@ function cardsApplyFields(idx, keepTar, keepCntr) {
const c = cards[parseInt(idx)];
if (!c) return false;
// 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
// card's Minimum SPI1 (TS 102 226 8.2.1.3.2.4). keepTar leaves the packet
// TAR alone (the plain send may target a manually entered TAR): its SPI1 is
// the operator's value too.
// chains) and its MSL + default keyset (v3.26.0): a preset apply selects
// the TAR in the selector and applies its keyset to the form, seeding the
// SPI1 from its MSL - the card's Minimum SPI1 (TS 102 226 8.2.1.3.2.4).
// keepTar leaves the packet TAR (and its keyset) alone: the plain send
// keeps the operator's selection.
spTarRender(c);
if (!keepTar) {
const roleTar = cardsRoleTar(c, _spTarKey);
document.getElementById('sp-tar').value = roleTar;
const msl = cardsTarMsl(c, roleTar) || CARDS_MSL_DEFAULT;
const spi1Sel = document.getElementById('sp-spi1');
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 sel = document.getElementById('sp-tar');
if (sel && Array.prototype.some.call(sel.options, o => o.value === roleTar)) {
sel.value = roleTar;
}
}
// The keyset: keep the keyset number the form carries when this preset
// defines it, otherwise start from the preset's first keyset.
const kicNow = (document.getElementById('sp-kic-hex') || {}).value;
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 || '';
const entry = cardsTarEntry(c, sel ? sel.value : roleTar);
spSpi1Seed((entry && entry.msl) || CARDS_MSL_DEFAULT);
const ks = (entry && entry.keyset) ? spKeysetFor(c, entry.keyset) : null;
if (ks) spKeysetApply(ks);
}
// 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
@@ -14301,8 +14265,6 @@ function cardsApplyFields(idx, keepTar, keepCntr) {
if (cntrEl) cntrEl.value = '';
}
spCntrPlaceholder();
if (!keepTar) document.getElementById('sp-tar').value = cardsRoleTar(c, _spTarKey);
spTarListRender(c);
spInvalidate();
updateSp();
spKeysetSync();
@@ -14317,7 +14279,7 @@ function cardsApply(idx, keepTar, keepCntr) {
// warning must not outlive the selection it described.
spKeysetSync();
spPresetWarningRender();
spTarListRender(null);
spTarRender(null);
return;
}
_genSpBuild();
@@ -23972,6 +23934,15 @@ const LANG_RU = {
'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 пакета.',
'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',
'Remove TAR': 'Удалить TAR',
'Description (optional)': 'Описание (необязательно)',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v433';
const CACHE = 'simple-v434';
const URLS = [
'index.html',
'help.html',
+23 -8
View File
@@ -34,12 +34,12 @@ for (const fn of ['cardsTarList', 'cardsRoleDefault', 'cardsRoleTar',
'spPresetCounterValue', 'spCntrPlaceholder', 'spCntrEffective', 'spCntrAhead',
'spCntrAdjust',
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
'spKeysetOptionsHtml', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender',
'spTarOptionsHtml', 'spTarRender', 'spSpi1Seed', 'spTarKeysetGuard', 'cardsTarKeysetCell', 'cardsTarKeysetFromForm', 'cardsTarKeysetRefresh', 'cardsTarRowsRender', 'cardsTarRowHtml', 'cardsTarsFromTable', 'cardsFreeTarsFromForm', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender',
'cardsKeysetAdd', 'cardsKeysetRemove', 'cardsKeysetKvnUpdate',
'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply',
'spKeysetChanged', 'ramKeysetChanged', 'ramPresetIdx', 'tarPresetIdx', 'spKeysetGuard',
'spTarListRender',
'ramPresetIdx', 'tarPresetIdx', 'spKeysetGuard',
'ramKeysetGuard',
'esc',
]) {
code += extractFunc(html, fn) + '\n';
}
@@ -61,9 +61,22 @@ globalThis.t = s => s;
function fakeEnv(selValue, selId, presetCntr) {
const els = {};
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: '' };
}
// 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 real form): the fake preset's keyset resolves to kvn 1
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');
});
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');
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(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.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);
assert.strictEqual(env.els['sp-tar'].value, 'B00000');
});
+78 -37
View File
@@ -35,13 +35,13 @@ for (const fn of ['cardsFormValues', 'cardsClearForm', 'cardsEdit',
'spCntrAhead', 'spCntrLocalSync', 'ramSaveCntr',
'cardsApplyFields', 'cardsApply', 'ramApplyCard',
'cardsScp80Complete', 'cardsScp81Complete', 'cardsAdmPresent',
'spTarKeyForPack', 'spPresetTar', 'spTarListRender', 'packToSp',
'spTarKeyForPack', 'spPresetTar', 'packToSp',
'esc', 'escHtml',
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
'spKeysetOptionsHtml', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender',
'spTarOptionsHtml', 'spTarRender', 'spSpi1Seed', 'spTarKeysetGuard', 'cardsTarKeysetCell', 'cardsTarKeysetFromForm', 'cardsTarKeysetRefresh', 'cardsTarRowsRender', 'cardsTarRowHtml', 'cardsTarsFromTable', 'cardsFreeTarsFromForm', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender',
'cardsKeysetAdd', 'cardsKeysetRemove', 'cardsKeysetKvnUpdate',
'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply', 'spKeysetChanged',
'ramKeysetChanged', 'ramPresetIdx', 'tarPresetIdx', 'spKeysetGuard', 'ramKeysetGuard',
'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply',
'ramPresetIdx', 'tarPresetIdx', 'spKeysetGuard', 'ramKeysetGuard',
'cardsTarList', 'cardsRoleDefault', 'cardsRoleTar',
'cardsTarEntry', 'cardsTarMsl', 'cardsTarText', 'spMslWarningText',
'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
'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-3-tar','cards-tr-3-msl','cards-tr-3-desc',
'cards-tr-4-tar','cards-tr-4-msl','cards-tr-4-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-keyset'];
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-cntr','sp-kic-key','sp-kid-key','sp-apdu','sp-result','sp-spi1-hex','sp-tar-list',
'sp-keyset-sel','ram-keyset-sel'];
'sp-cntr','sp-kic-key','sp-kid-key','sp-apdu','sp-result','sp-spi1-hex'];
const CHAIN_IDS = ['chain-sim-preview','chain-usim-preview','chain-ram-preview',
'ber-result','hota-preview'];
@@ -91,10 +90,10 @@ function ksRow(els, i, ks) {
}
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;
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 || {});
}
// 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() {
const classes = new Set();
return {
@@ -134,6 +161,7 @@ let apiCalls = [];
function setup() {
const els = {};
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.cards = [];
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"/);
// ... and the table reads back as the three role entries
assert.deepStrictEqual(cardsTarsFromForm(), [
{ role: 'isd', tar: '000000', msl: '16', desc: '' },
{ role: 'uiccRfm', tar: 'B00000', msl: '16', desc: '' },
{ role: 'usimRfm', tar: 'B00001', msl: '16', desc: '' },
{ role: 'isd', tar: '000000', msl: '16', desc: '', keyset: null },
{ role: 'uiccRfm', tar: 'B00000', msl: '16', desc: '', keyset: null },
{ 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;
const v = cardsFormValues();
assert.deepStrictEqual(v.tars, [
{ role: 'isd', tar: '000001', msl: '0A', desc: '' },
{ role: 'uiccRfm', tar: 'B00000', msl: '16', desc: '' },
{ role: 'usimRfm', tar: 'B00003', msl: '1A', desc: '' },
{ tar: 'AF4D02', msl: '0A', desc: 'My applet' },
{ role: 'isd', tar: '000001', msl: '0A', desc: '', keyset: null },
{ role: 'uiccRfm', tar: 'B00000', msl: '16', desc: '', keyset: null },
{ role: 'usimRfm', tar: 'B00003', msl: '1A', desc: '', keyset: null },
{ tar: 'AF4D02', msl: '0A', desc: 'My applet', keyset: null },
]);
assert.strictEqual(v.pin1, '1234');
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';
ksRow(els, 0, { kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '0000000001' });
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
// start empty - the defaults fill in
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, [
{ kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '0000000001' }]);
assert.deepStrictEqual(apiCalls[0].body.tars, [
{ role: 'isd', tar: '000000', msl: '16', desc: '' },
{ role: 'uiccRfm', tar: 'B00000', msl: '16', desc: '' },
{ role: 'usimRfm', tar: 'B00001', msl: '16', desc: '' },
{ role: 'isd', tar: '000000', msl: '16', desc: '', keyset: 1 },
{ role: 'uiccRfm', tar: 'B00000', msl: '16', desc: '', keyset: 1 },
{ role: 'usimRfm', tar: 'B00001', msl: '16', desc: '', keyset: 1 },
]);
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);
assert.strictEqual(els['sp-tar'].value, 'B0000D');
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._spTarKey = 'isd';
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');
});
@@ -494,15 +530,18 @@ test('spPresetTar reads the selected preset and falls back to the spec defaults'
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();
spTarListRender(presetFixture({ tars: [
{ role: 'isd', tar: '000000', msl: '16' },
spTarRender(presetFixture({ tars: [
{ role: 'isd', tar: '000000', msl: '16', keyset: 1 },
{ 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-list'].innerHTML, /value="AF4D02"[^>]*label="My applet \(MSL 0A\)"/);
spTarListRender(null);
assert.strictEqual(els['sp-tar-list'].innerHTML, '');
assert.match(els['sp-tar'].innerHTML, /value="000000"[^>]*>000000 \u00b7 ISD \u00b7 MSL 16 \u00b7 Keyset 1</);
assert.match(els['sp-tar'].innerHTML, /value="AF4D02"[^>]*>AF4D02 \u00b7 My applet \u00b7 MSL 0A</);
spTarRender(null);
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', () => {
@@ -533,17 +572,18 @@ test('packing a chain selects the TAR of its target', () => {
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();
els['chain-sim-preview'].value = 'A0A4000002';
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');
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', () => {
@@ -586,7 +626,8 @@ test('the TAR table renders the role rows and adds/removes free rows', () => {
assert.strictEqual(_cardsTarCount, 2);
// the new row renders empty
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
cardsTarRemove(0);
assert.strictEqual(_cardsTarCount, 2);
+6
View File
@@ -43,6 +43,12 @@ code += extractFunc(html, 'cardsScp80Complete') + '\n';
code += extractFunc(html, 'cardsScp81Complete') + '\n';
code += extractFunc(html, 'cardsMatchedPreset') + '\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, 'ramPresetIdx') + '\n';
code += extractFunc(html, 'tarPresetIdx') + '\n';
+46 -57
View File
@@ -23,8 +23,9 @@ function extractFunc(src, name) {
let code = '';
for (const fn of ['spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
'spKeysetOptionsHtml', 'spCounterTracked', 'spPresetIdx', 'ramPresetIdx',
'tarPresetIdx', 'spKeysetSync', 'ramRender', 'esc', 'escHtml']) {
'spTarOptionsHtml', 'spTarRender', 'spSpi1Seed', 'spTarKeysetGuard', 'cardsTarKeysetCell', 'cardsTarKeysetFromForm', 'cardsTarKeysetRefresh', 'cardsTarRowsRender', 'cardsTarRowHtml', 'cardsTarsFromTable', 'cardsFreeTarsFromForm', 'spCounterTracked', 'spPresetIdx', 'ramPresetIdx',
'tarPresetIdx', 'spKeysetSync', 'ramRender', 'esc', 'escHtml',
'cardsTarList', 'cardsTarEntry', 'cardsRoleTar']) {
code += extractFunc(html, fn) + '\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/);
});
test('spKeysetOptionsHtml lists the keysets and marks an undefined number', () => {
const html1 = spKeysetOptionsHtml(PRESET, '15', '15');
assert.match(html1, /value="1" selected/);
assert.match(html1, /value="2"/);
assert.ok(html1.indexOf('0000000005') > 0, 'the counters are shown');
// a version the preset does not define is offered but marked
const html2 = spKeysetOptionsHtml(PRESET, '35', '35');
assert.match(html2, /value="3" selected/);
assert.match(html2, /not defined in the preset/);
// a preset without keysets: the form's version shows as undefined
const html3 = spKeysetOptionsHtml({ keysets: [] }, '15', '15');
assert.match(html3, /not defined in the preset/);
// no keyset number in the form and no keysets at all
assert.match(spKeysetOptionsHtml({}, '', ''), /no keyset/);
test('spTarKeysetGuard follows the TAR default keyset (v3.26.0)', () => {
const preset = Object.assign({}, PRESET, { tars: [
{ role: 'isd', tar: '000000', msl: '16', keyset: 1 },
{ role: 'uiccRfm', tar: 'B00000', msl: '16', keyset: 2 },
{ role: 'usimRfm', tar: 'B00001', msl: '00' },
{ tar: 'B00011', msl: '16', keyset: 3 }] });
assert.strictEqual(spTarKeysetGuard(preset, '000000'), '');
assert.strictEqual(spTarKeysetGuard(preset, 'B00001'), ''); // MSL 00: keyless is fine
// a TAR naming a keyset the preset does not define
assert.match(spTarKeysetGuard(preset, 'B00011'), /is not defined in the preset/);
// a security TAR without a default keyset
const noKeyset = Object.assign({}, PRESET, { tars: [
{ role: 'isd', tar: '000000', msl: '16' }] });
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', () => {
@@ -117,9 +121,14 @@ test('the Cards form wires the keyset editor', () => {
// 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 \+ '-cntr/);
// the selectors exist in both SCP80 views
assert.match(html, /id="sp-keyset-sel" onchange="spKeysetChanged\(\)"/);
assert.match(html, /id="ram-keyset-sel" onchange="ramKeysetChanged\(\)"/);
// v3.26.0: the keyset selectors are gone - the SCP80 TAR select drives
// the keyset, the RAM operations use the preset's ISD entry, and the TAR
// 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
assert.ok(html.includes('const ksErr = spKeysetGuard();'));
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);
});
test('ramRender rebuilds the keyset selector from the remembered preset', () => {
// 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"
test('ramRender keeps the remembered preset (v3.26.0: no keyset selector)', () => {
const els = {
'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-kid-hex': { value: '25' },
};
@@ -160,9 +164,6 @@ test('ramRender rebuilds the keyset selector from the remembered preset', () =>
ramRender();
assert.strictEqual(els['ram-card-sel'].value, '0',
'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', () => {
@@ -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(); }"));
});
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 = {
'sp-keyset-sel': { value: '', innerHTML: '' },
'ram-keyset-sel': { value: '', innerHTML: '' },
'tar-keyset-sel': { value: '', innerHTML: '' },
'sp-tar': { value: '', disabled: false, innerHTML: '', options: [] },
'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.cards = [];
spKeysetSync();
for (const id of ['sp-keyset-sel', 'ram-keyset-sel', 'tar-keyset-sel']) {
assert.strictEqual(els[id].disabled, true, id + ' must be disabled');
assert.match(els[id].innerHTML, /select a card preset/);
assert.ok(!els[id].innerHTML.includes('not defined'), id);
}
// a selected preset enables the selector and lists its keysets
globalThis.cards = [{ name: 'EP', keysets: [
{ kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '0000000001' }] }];
spTarRender();
assert.strictEqual(els['sp-tar'].disabled, true, 'no preset: the selector waits');
assert.match(els['sp-tar'].innerHTML, /select a card preset/);
// a selected preset enables the selector and lists its TARs with their
// MSL and default keyset
globalThis.cards = [{ name: 'EP', tars: [
{ role: 'isd', tar: '000000', msl: '16', keyset: 1 },
{ tar: 'AF4D02', msl: '0A', desc: 'My applet' }] }];
els['sp-card-sel'].value = '0';
els['ram-card-sel'].value = '0';
els['tar-card-sel'].value = '0';
els['sp-kic-hex'].value = '15';
els['sp-kid-hex'].value = '15';
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/);
}
spTarRender();
assert.strictEqual(els['sp-tar'].disabled, false);
assert.match(els['sp-tar'].innerHTML, /value="000000"[^>]*>000000 \u00b7 ISD \u00b7 MSL 16 \u00b7 Keyset 1</);
assert.match(els['sp-tar'].innerHTML, /value="AF4D02"[^>]*>AF4D02 \u00b7 My applet \u00b7 MSL 0A</);
});
test('clearing the preset selection clears the keyset selectors', () => {
test('clearing the preset selection resets the TAR selector', () => {
const at = html.indexOf('function cardsApply(');
assert.ok(at > 0);
const body = html.slice(at, at + 600);
assert.ok(body.includes('spKeysetSync();'),
'cardsApply must sync the keyset selectors when no preset is selected');
assert.ok(body.includes('spTarRender(null);'),
'cardsApply must reset the TAR selector when no preset is selected');
});
+13 -10
View File
@@ -30,9 +30,9 @@ const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamB
'spCntrLow', 'ramCntrLowHtml', 'ramCntrLowPresetIdx', 'ramCntrLowView', 'ramShowCntrLow',
'ramCntrLowBox', 'ramCntrLowClear',
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
'spKeysetOptionsHtml', 'spPresetIdx', 'tarPresetIdx',
'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply', 'spKeysetChanged',
'ramKeysetChanged', 'ramPresetIdx', 'spKeysetGuard', 'ramKeysetGuard',
'spTarOptionsHtml', 'spTarRender', 'spSpi1Seed', 'spTarKeysetGuard', 'cardsTarKeysetCell', 'cardsTarKeysetFromForm', 'cardsTarKeysetRefresh', 'cardsTarRowsRender', 'cardsTarRowHtml', 'cardsTarsFromTable', 'cardsFreeTarsFromForm', 'spPresetIdx', 'tarPresetIdx',
'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply',
'ramPresetIdx', 'spKeysetGuard', 'ramKeysetGuard',
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn', 'ramCapToolkitMode', 'ramOpProgressText',
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
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("getElementById('cards-ks-' + row + '-cntr')"), go);
// 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');
assert.ok(sync.includes("'/api/counter-probe'"), sync);
assert.ok(sync.includes("own = view === 'sp'"), sync);
assert.ok(sync.includes('kvnOf(own)'), sync);
assert.ok(sync.includes('kvnOf(other)'), sync);
assert.ok(sync.includes('spKeysetSync()'), 'the keyset labels must refresh');
assert.ok(sync.includes("view === 'ram' ? cardsRoleTar(preset, 'isd')"), sync);
assert.ok(sync.includes('spKeysetSync()'), 'the availability must refresh');
assert.ok(sync.includes('spCntrPlaceholder()'),
'the stored value must refresh the field placeholder (the field stays an override)');
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);
});
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');
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', () => {
+8 -2
View File
@@ -35,14 +35,20 @@ for (const fn of ['berLenStr', 'ramDeleteApdu', 'ramIncrementCntr', 'ramSaveCntr
'ramCntrLowBox', 'ramCntrLowClear',
'escHtml', 'esc',
'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',
'spKeysetChanged', 'ramKeysetChanged', 'ramPresetIdx', 'spKeysetGuard',
'ramPresetIdx', 'spKeysetGuard',
'ramKeysetGuard',
]) {
code += extractFunc(html, fn) + '\n';
}
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);
function fakeEnv(por, success) {
+33 -65
View File
@@ -23,78 +23,43 @@ function extractFunc(src, name) {
let code = '';
for (const fn of ['esc', 'escHtml', 'tarProbeVerdictLabel', 'tarProbeHtml',
'tarProbeRowsHtml', 'tarProbeCustomLoad', 'tarProbeCustomSave',
'tarProbeAllRows', 'tarProbeChecked']) {
'tarProbeRowsHtml', 'tarProbeAllRows', 'tarProbeChecked', 'cardsTarList']) {
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';
eval(code);
function fakeStorage() {
const map = {};
globalThis.localStorage = {
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', () => {
test('tarProbeRowsHtml renders the preset rows checked', () => {
// v3.26.0: the rows come from the preset - no custom rows, no remove
// buttons
const out = tarProbeRowsHtml([
{ tar: '000000', label: 'Issuer Security Domain (compact)', doc: 'TS 102 226' },
{ tar: '123456', label: 'my applet', custom: true },
{ tar: '000000', label: 'ISD', doc: 'MSL 16 \u00b7 Keyset 1' },
{ tar: 'AF4D02', label: 'My applet' },
]);
assert.match(out, /value="000000" checked/);
assert.match(out, /title="TS 102 226"/);
assert.match(out, /Issuer Security Domain \(compact\)/);
assert.match(out, /value="123456" checked/);
assert.match(out, /tarProbeRemoveCustom\('123456'\)/, 'custom rows get a remove button');
assert.ok(!/tarProbeRemoveCustom\('000000'\)/.test(out), 'standard rows have none');
assert.match(out, /title="MSL 16/);
assert.match(out, /ISD/);
assert.match(out, /value="AF4D02" checked/);
assert.ok(!out.includes('tarProbeRemoveCustom'), 'no custom rows anymore');
// 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('<i>x'));
});
test('custom TARs round-trip through localStorage (best effort)', () => {
fakeStorage();
assert.deepStrictEqual(tarProbeCustomLoad(), []);
tarProbeCustomSave([{ tar: '123456', label: 'mine' }, { tar: 'aabbcc' }]);
const back = tarProbeCustomLoad();
assert.deepStrictEqual(back, [{ tar: '123456', label: 'mine', custom: true },
{ tar: 'AABBCC', label: '', custom: true }]);
// garbage is dropped, never fatal
localStorage.setItem('simple_tar_probe', '{not json');
assert.deepStrictEqual(tarProbeCustomLoad(), []);
localStorage.setItem('simple_tar_probe', JSON.stringify([{ tar: 'zz' }, { tar: 'B0B0B0' }]));
assert.deepStrictEqual(tarProbeCustomLoad().map(e => e.tar), ['B0B0B0']);
});
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('tarProbeAllRows lists the selected preset TARs (v3.26.0)', () => {
globalThis.cards = [{ name: 'C', tars: [
{ role: 'isd', tar: '000000', msl: '16', keyset: 1 },
{ tar: 'AF4D02', msl: '0A', desc: 'My applet' }] }];
globalThis.tarPresetIdx = () => 0;
assert.deepStrictEqual(tarProbeAllRows(), [
{ tar: '000000', label: 'ISD', doc: 'MSL 16 \u00b7 Keyset 1' },
{ tar: 'AF4D02', label: 'My applet', doc: 'MSL 0A' },
]);
// no preset: no rows
globalThis.tarPresetIdx = () => -1;
assert.deepStrictEqual(tarProbeAllRows(), []);
delete globalThis.tarPresetIdx;
});
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.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');
// 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-keyset-sel" onchange="tarKeysetChanged\(\)"/);
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, /onclick="tarProbeSelectAll\(true\)"/);
assert.match(html, /onclick="tarProbeAddCustom\(\)"/);
assert.match(html, /id="tar-apdu"[^>]*value="00A40000023F00"/);
assert.match(html, /onclick="tarExecute\(\)"/);
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('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);'));
// the RAM-era leftovers are gone
assert.ok(!html.includes('id="ram-tar-rows"'));