diff --git a/frontend/index.html b/frontend/index.html index 8a969ab..2a46ed2 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1436,7 +1436,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 = '2.8.0'; +const SIMPLE_VERSION = '2.8.1'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== diff --git a/frontend/sw.js b/frontend/sw.js index 8c4756c..b60a1fe 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v225'; +const CACHE = 'simple-v226'; const URLS = [ 'index.html', 'help.html', diff --git a/pyproject.toml b/pyproject.toml index 2974e6f..e4dd4fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "2.8.0" +version = "2.8.1" description = "HTTP REST server wrapping pysim for the SIMple PWA" requires-python = ">=3.8" # pysim is a git-only dependency installed explicitly by setup.bat/setup.sh. diff --git a/pysim_simple_server/fastinit.py b/pysim_simple_server/fastinit.py index 6801dae..1d962f7 100644 --- a/pysim_simple_server/fastinit.py +++ b/pysim_simple_server/fastinit.py @@ -80,6 +80,18 @@ def pick_profile_no_reset(scc): scc.reset_card = original_reset +def _restore_mf_after_probe(scc): + """Probing can leave an ADF (e.g. ISD-R on an eUICC) selected. The + RuntimeState construction selects MF by FID, which some cards refuse from + within an ADF (6A82), so restore it here: the cheap select keeps the + reset-free path for normal cards, the physical reset covers the rest.""" + try: + scc.select_file('3f00') + except SwMatchError: + sys.stderr.write('FAST-INIT: MF restore after probing failed; physical reset\n') + scc.reset_card() + + def init_card_fast(sl, skip_card_init=False, wait=True): """Replacement for pySim.app.init_card() that avoids redundant resets. @@ -112,6 +124,11 @@ def _init_card_once(sl, skip_card_init, wait): if profile is None: return None, card + # A successful probe may leave an ADF selected (e.g. ISD-R on an eUICC); + # RuntimeState selects MF by FID and would fail on cards that refuse that + # from within an ADF. + _restore_mf_after_probe(scc) + if generic_card and isinstance(profile, CardProfileUICC): card._adm_chv_num = 0x0A @@ -137,7 +154,9 @@ def _init_card_once(sl, skip_card_init, wait): except SwMatchError: pass finally: - rs.soft_reset() + # rs.reset() tries the reset-free soft reset first and escalates + # to a physical reset when MF cannot be selected from the ADF. + rs.reset() return rs, card diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index fc25ef1..6e618e3 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -27,7 +27,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '2.8.0' +VERSION = '2.8.1' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE diff --git a/tests/test_fastinit.py b/tests/test_fastinit.py index b46eb1e..914968b 100644 --- a/tests/test_fastinit.py +++ b/tests/test_fastinit.py @@ -24,17 +24,21 @@ from pysim_simple_server.fastinit import ( class FakeScc: - def __init__(self): + def __init__(self, mf_select_error=False): self.sel_ctrl = '0004' self.cla_byte = '00' self.resets = 0 self.selected = [] + self.mf_select_error = mf_select_error def reset_card(self): self.resets += 1 def select_file(self, fid): self.selected.append(fid) + if fid == '3f00' and self.mf_select_error: + # eUICC with the ISD-R ADF selected: MF is not selectable by FID + raise SwMatchError('6a82', '9000') return ('', '9000') def select_adf(self, aid): @@ -56,6 +60,19 @@ class TestPickProfileNoReset(unittest.TestCase): self.assertEqual(scc.resets, 1) +class TestRestoreMfAfterProbe(unittest.TestCase): + def test_normal_card_keeps_the_reset_free_path(self): + scc = FakeScc() + fastinit._restore_mf_after_probe(scc) + self.assertEqual(scc.resets, 0) + self.assertEqual(scc.selected[-1], '3f00') + + def test_adf_selected_card_falls_back_to_a_physical_reset(self): + scc = FakeScc(mf_select_error=True) + fastinit._restore_mf_after_probe(scc) + self.assertEqual(scc.resets, 1) + + class FakeLchan: def __init__(self): self.scc = types.SimpleNamespace(scp=object())