blob: d6d9a644a82330a75aed0f5704d6904857263014 [file] [log] [blame]
Harald Welteb2edd142021-01-08 23:29:35 +01001#!/usr/bin/env python3
2
3# Interactive shell for working with SIM / UICC / USIM / ISIM cards
4#
Harald Weltecab26c72022-08-06 16:12:30 +02005# (C) 2021-2022 by Harald Welte <laforge@osmocom.org>
Harald Welteb2edd142021-01-08 23:29:35 +01006#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20from typing import List
21
22import json
Philipp Maier5d698e52021-09-16 13:18:01 +020023import traceback
Harald Welteb2edd142021-01-08 23:29:35 +010024
25import cmd2
Harald Welte93aac3a2023-04-27 15:18:13 +020026from packaging import version
Harald Welte961b8032023-04-27 17:30:22 +020027from cmd2 import style
28# cmd2 >= 2.3.0 has deprecated the bg/fg in favor of Bg/Fg :(
29if version.parse(cmd2.__version__) < version.parse("2.3.0"):
Oliver Smithe47ea5f2023-05-23 12:54:15 +020030 from cmd2 import fg, bg # pylint: disable=no-name-in-module
Harald Welte961b8032023-04-27 17:30:22 +020031 RED = fg.red
32 LIGHT_RED = fg.bright_red
33 LIGHT_GREEN = fg.bright_green
34else:
35 from cmd2 import Fg, Bg # pylint: disable=no-name-in-module
36 RED = Fg.RED
37 LIGHT_RED = Fg.LIGHT_RED
38 LIGHT_GREEN = Fg.LIGHT_GREEN
Harald Welteb2edd142021-01-08 23:29:35 +010039from cmd2 import CommandSet, with_default_category, with_argparser
40import argparse
41
42import os
43import sys
Philipp Maier2b11c322021-03-17 12:37:39 +010044from pathlib import Path
Philipp Maier76667642021-09-22 16:53:22 +020045from io import StringIO
Harald Welteb2edd142021-01-08 23:29:35 +010046
Harald Weltecab26c72022-08-06 16:12:30 +020047from pprint import pprint as pp
48
Harald Welteb2edd142021-01-08 23:29:35 +010049from pySim.exceptions import *
50from pySim.commands import SimCardCommands
Harald Weltecab26c72022-08-06 16:12:30 +020051from pySim.transport import init_reader, ApduTracer, argparse_add_reader_args, ProactiveHandler
Philipp Maierbb73e512021-05-05 16:14:00 +020052from pySim.cards import card_detect, SimCard
Harald Welte1e52b0d2022-07-16 11:53:21 +020053from pySim.utils import h2b, swap_nibbles, rpad, b2h, JsonEncoder, bertlv_parse_one, sw_match
Harald Weltecab26c72022-08-06 16:12:30 +020054from pySim.utils import sanitize_pin_adm, tabulate_str_list, boxed_heading_str, Hexstr
Philipp Maier76667642021-09-22 16:53:22 +020055from pySim.card_handler import CardHandler, CardHandlerAuto
Harald Welteb2edd142021-01-08 23:29:35 +010056
Harald Welte1e52b0d2022-07-16 11:53:21 +020057from pySim.filesystem import RuntimeState, CardDF, CardADF, CardModel
Philipp Maiera028c7d2021-11-08 16:12:03 +010058from pySim.profile import CardProfile
Vadim Yanitskiy87dd0202023-04-22 20:45:29 +070059from pySim.cdma_ruim import CardProfileRUIM
Harald Welteb2edd142021-01-08 23:29:35 +010060from pySim.ts_102_221 import CardProfileUICC
Harald Welte3c9b7842021-10-19 21:44:24 +020061from pySim.ts_102_222 import Ts102222Commands
Harald Welte5ce35242021-04-02 20:27:05 +020062from pySim.ts_31_102 import CardApplicationUSIM
63from pySim.ts_31_103 import CardApplicationISIM
Harald Weltee6191052023-06-06 10:32:46 +020064from pySim.ts_31_104 import CardApplicationHPSIM
Harald Welte95ce6b12021-10-20 18:40:54 +020065from pySim.ara_m import CardApplicationARAM
Harald Welte34eb5042022-02-21 17:19:28 +010066from pySim.global_platform import CardApplicationISD
Harald Welte2a33ad22021-10-20 10:14:18 +020067from pySim.gsm_r import DF_EIRENE
Harald Weltecab26c72022-08-06 16:12:30 +020068from pySim.cat import ProactiveCommand
Harald Welteb2edd142021-01-08 23:29:35 +010069
Harald Welte4c1dca02021-10-14 17:48:25 +020070# we need to import this module so that the SysmocomSJA2 sub-class of
71# CardModel is created, which will add the ATR-based matching and
72# calling of SysmocomSJA2.add_files. See CardModel.apply_matching_models
Harald Weltef44256c2021-10-14 15:53:39 +020073import pySim.sysmocom_sja2
74
Harald Welte4442b3d2021-04-03 09:00:16 +020075from pySim.card_key_provider import CardKeyProviderCsv, card_key_provider_register, card_key_provider_get_field
Philipp Maier2b11c322021-03-17 12:37:39 +010076
Harald Weltec91085e2022-02-10 18:05:45 +010077
Philipp Maierea95c392021-09-16 13:10:19 +020078def init_card(sl):
Harald Weltec91085e2022-02-10 18:05:45 +010079 """
80 Detect card in reader and setup card profile and runtime state. This
81 function must be called at least once on startup. The card and runtime
82 state object (rs) is required for all pySim-shell commands.
83 """
Philipp Maierea95c392021-09-16 13:10:19 +020084
Harald Weltec91085e2022-02-10 18:05:45 +010085 # Wait up to three seconds for a card in reader and try to detect
86 # the card type.
87 print("Waiting for card...")
88 try:
89 sl.wait_for_card(3)
90 except NoCardError:
91 print("No card detected!")
92 return None, None
93 except:
94 print("Card not readable!")
95 return None, None
Philipp Maierea95c392021-09-16 13:10:19 +020096
Philipp Maier24031252022-06-14 16:16:42 +020097 generic_card = False
Harald Weltec91085e2022-02-10 18:05:45 +010098 card = card_detect("auto", scc)
99 if card is None:
100 print("Warning: Could not detect card type - assuming a generic card type...")
101 card = SimCard(scc)
Philipp Maier24031252022-06-14 16:16:42 +0200102 generic_card = True
Philipp Maiera028c7d2021-11-08 16:12:03 +0100103
Harald Weltec91085e2022-02-10 18:05:45 +0100104 profile = CardProfile.pick(scc)
105 if profile is None:
106 print("Unsupported card type!")
Philipp Maier7226c092022-06-01 17:58:38 +0200107 return None, card
Philipp Maierea95c392021-09-16 13:10:19 +0200108
Philipp Maier24031252022-06-14 16:16:42 +0200109 # ETSI TS 102 221, Table 9.3 specifies a default for the PIN key
110 # references, however card manufactures may still decide to pick an
111 # arbitrary key reference. In case we run on a generic card class that is
112 # detected as an UICC, we will pick the key reference that is officially
113 # specified.
114 if generic_card and isinstance(profile, CardProfileUICC):
115 card._adm_chv_num = 0x0A
116
Harald Weltec91085e2022-02-10 18:05:45 +0100117 print("Info: Card is of type: %s" % str(profile))
Philipp Maiera028c7d2021-11-08 16:12:03 +0100118
Harald Weltec91085e2022-02-10 18:05:45 +0100119 # FIXME: This shouln't be here, the profile should add the applications,
120 # however, we cannot simply put his into ts_102_221.py since we would
121 # have to e.g. import CardApplicationUSIM from ts_31_102.py, which already
122 # imports from ts_102_221.py. This means we will end up with a circular
123 # import, which needs to be resolved first.
124 if isinstance(profile, CardProfileUICC):
125 profile.add_application(CardApplicationUSIM())
126 profile.add_application(CardApplicationISIM())
Harald Weltee6191052023-06-06 10:32:46 +0200127 profile.add_application(CardApplicationHPSIM())
Harald Weltec91085e2022-02-10 18:05:45 +0100128 profile.add_application(CardApplicationARAM())
Harald Welte34eb5042022-02-21 17:19:28 +0100129 profile.add_application(CardApplicationISD())
Philipp Maiera028c7d2021-11-08 16:12:03 +0100130
Harald Weltec91085e2022-02-10 18:05:45 +0100131 # Create runtime state with card profile
132 rs = RuntimeState(card, profile)
Philipp Maierea95c392021-09-16 13:10:19 +0200133
Harald Weltec91085e2022-02-10 18:05:45 +0100134 # FIXME: This is an GSM-R related file, it needs to be added throughout,
135 # the profile. At the moment we add it for all cards, this won't hurt,
136 # but regular SIM and UICC will not have it and fail to select it.
137 rs.mf.add_file(DF_EIRENE())
Philipp Maierea95c392021-09-16 13:10:19 +0200138
Harald Weltec91085e2022-02-10 18:05:45 +0100139 CardModel.apply_matching_models(scc, rs)
Harald Weltef44256c2021-10-14 15:53:39 +0200140
Harald Weltec91085e2022-02-10 18:05:45 +0100141 # inform the transport that we can do context-specific SW interpretation
142 sl.set_sw_interpreter(rs)
Philipp Maierea95c392021-09-16 13:10:19 +0200143
Harald Weltec91085e2022-02-10 18:05:45 +0100144 return rs, card
145
Philipp Maier2b11c322021-03-17 12:37:39 +0100146
Harald Welteb2edd142021-01-08 23:29:35 +0100147class PysimApp(cmd2.Cmd):
Harald Weltec91085e2022-02-10 18:05:45 +0100148 CUSTOM_CATEGORY = 'pySim Commands'
Philipp Maier76667642021-09-22 16:53:22 +0200149
Harald Weltec91085e2022-02-10 18:05:45 +0100150 def __init__(self, card, rs, sl, ch, script=None):
Harald Weltec85d4062023-04-27 17:10:17 +0200151 if version.parse(cmd2.__version__) < version.parse("2.0.0"):
152 kwargs = {'use_ipython': True}
153 else:
154 kwargs = {'include_ipy': True}
155
Oliver Smithe47ea5f2023-05-23 12:54:15 +0200156 # pylint: disable=unexpected-keyword-arg
Harald Weltec91085e2022-02-10 18:05:45 +0100157 super().__init__(persistent_history_file='~/.pysim_shell_history', allow_cli_args=False,
Harald Weltec85d4062023-04-27 17:10:17 +0200158 auto_load_commands=False, startup_script=script, **kwargs)
Harald Welte961b8032023-04-27 17:30:22 +0200159 self.intro = style('Welcome to pySim-shell!', fg=RED)
Harald Weltec91085e2022-02-10 18:05:45 +0100160 self.default_category = 'pySim-shell built-in commands'
161 self.card = None
162 self.rs = None
Harald Weltea6c0f882022-07-17 14:23:17 +0200163 self.lchan = None
164 self.py_locals = {'card': self.card, 'rs': self.rs, 'lchan': self.lchan}
Harald Weltec91085e2022-02-10 18:05:45 +0100165 self.sl = sl
166 self.ch = ch
Harald Welte1748b932021-04-06 21:12:25 +0200167
Harald Weltec91085e2022-02-10 18:05:45 +0100168 self.numeric_path = False
Harald Weltec91085e2022-02-10 18:05:45 +0100169 self.conserve_write = True
Harald Weltec91085e2022-02-10 18:05:45 +0100170 self.json_pretty_print = True
Harald Weltec91085e2022-02-10 18:05:45 +0100171 self.apdu_trace = False
Philipp Maier5d698e52021-09-16 13:18:01 +0200172
Harald Welte93aac3a2023-04-27 15:18:13 +0200173 if version.parse(cmd2.__version__) < version.parse("2.0.0"):
Oliver Smithe47ea5f2023-05-23 12:54:15 +0200174 # pylint: disable=no-value-for-parameter
Harald Welte93aac3a2023-04-27 15:18:13 +0200175 self.add_settable(cmd2.Settable('numeric_path', bool, 'Print File IDs instead of names',
176 onchange_cb=self._onchange_numeric_path))
Oliver Smithe47ea5f2023-05-23 12:54:15 +0200177 # pylint: disable=no-value-for-parameter
Harald Welte93aac3a2023-04-27 15:18:13 +0200178 self.add_settable(cmd2.Settable('conserve_write', bool, 'Read and compare before write',
179 onchange_cb=self._onchange_conserve_write))
Oliver Smithe47ea5f2023-05-23 12:54:15 +0200180 # pylint: disable=no-value-for-parameter
Harald Welte93aac3a2023-04-27 15:18:13 +0200181 self.add_settable(cmd2.Settable('json_pretty_print', bool, 'Pretty-Print JSON output'))
Oliver Smithe47ea5f2023-05-23 12:54:15 +0200182 # pylint: disable=no-value-for-parameter
Harald Welte93aac3a2023-04-27 15:18:13 +0200183 self.add_settable(cmd2.Settable('apdu_trace', bool, 'Trace and display APDUs exchanged with card',
184 onchange_cb=self._onchange_apdu_trace))
185 else:
186 self.add_settable(cmd2.Settable('numeric_path', bool, 'Print File IDs instead of names', self, \
187 onchange_cb=self._onchange_numeric_path)) # pylint: disable=too-many-function-args
188 self.add_settable(cmd2.Settable('conserve_write', bool, 'Read and compare before write', self, \
189 onchange_cb=self._onchange_conserve_write)) # pylint: disable=too-many-function-args
190 self.add_settable(cmd2.Settable('json_pretty_print', bool, 'Pretty-Print JSON output', self)) # pylint: disable=too-many-function-args
191 self.add_settable(cmd2.Settable('apdu_trace', bool, 'Trace and display APDUs exchanged with card', self, \
192 onchange_cb=self._onchange_apdu_trace)) # pylint: disable=too-many-function-args
Harald Weltec91085e2022-02-10 18:05:45 +0100193 self.equip(card, rs)
Philipp Maier5d698e52021-09-16 13:18:01 +0200194
Harald Weltec91085e2022-02-10 18:05:45 +0100195 def equip(self, card, rs):
196 """
197 Equip pySim-shell with the supplied card and runtime state, add (or remove) all required settables and
198 and commands to enable card operations.
199 """
Philipp Maier76667642021-09-22 16:53:22 +0200200
Harald Weltec91085e2022-02-10 18:05:45 +0100201 rc = False
Philipp Maier5d698e52021-09-16 13:18:01 +0200202
Harald Weltec91085e2022-02-10 18:05:45 +0100203 # Unequip everything from pySim-shell that would not work in unequipped state
204 if self.rs:
Harald Weltea6c0f882022-07-17 14:23:17 +0200205 lchan = self.rs.lchan[0]
206 lchan.unregister_cmds(self)
Harald Welte892526f2023-06-06 17:09:50 +0200207 for cmds in [Iso7816Commands, Ts102222Commands, PySimCommands]:
Harald Weltec91085e2022-02-10 18:05:45 +0100208 cmd_set = self.find_commandsets(cmds)
209 if cmd_set:
210 self.unregister_command_set(cmd_set[0])
Philipp Maier5d698e52021-09-16 13:18:01 +0200211
Harald Weltec91085e2022-02-10 18:05:45 +0100212 self.card = card
213 self.rs = rs
Philipp Maier5d698e52021-09-16 13:18:01 +0200214
Harald Weltec91085e2022-02-10 18:05:45 +0100215 # When a card object and a runtime state is present, (re)equip pySim-shell with everything that is
216 # needed to operate on cards.
217 if self.card and self.rs:
Harald Weltea6c0f882022-07-17 14:23:17 +0200218 self.lchan = self.rs.lchan[0]
Harald Weltec91085e2022-02-10 18:05:45 +0100219 self._onchange_conserve_write(
220 'conserve_write', False, self.conserve_write)
221 self._onchange_apdu_trace('apdu_trace', False, self.apdu_trace)
222 self.register_command_set(Iso7816Commands())
Harald Welte3c9b7842021-10-19 21:44:24 +0200223 self.register_command_set(Ts102222Commands())
Harald Weltec91085e2022-02-10 18:05:45 +0100224 self.register_command_set(PySimCommands())
225 self.iccid, sw = self.card.read_iccid()
Harald Weltea6c0f882022-07-17 14:23:17 +0200226 self.lchan.select('MF', self)
Harald Weltec91085e2022-02-10 18:05:45 +0100227 rc = True
228 else:
229 self.poutput("pySim-shell not equipped!")
Philipp Maier5d698e52021-09-16 13:18:01 +0200230
Harald Weltec91085e2022-02-10 18:05:45 +0100231 self.update_prompt()
232 return rc
Harald Welteb2edd142021-01-08 23:29:35 +0100233
Harald Weltec91085e2022-02-10 18:05:45 +0100234 def poutput_json(self, data, force_no_pretty=False):
235 """like cmd2.poutput() but for a JSON serializable dict."""
236 if force_no_pretty or self.json_pretty_print == False:
237 output = json.dumps(data, cls=JsonEncoder)
238 else:
239 output = json.dumps(data, cls=JsonEncoder, indent=4)
240 self.poutput(output)
Harald Welteb2edd142021-01-08 23:29:35 +0100241
Harald Weltec91085e2022-02-10 18:05:45 +0100242 def _onchange_numeric_path(self, param_name, old, new):
243 self.update_prompt()
Philipp Maier38c74f62021-03-17 17:19:52 +0100244
Harald Weltec91085e2022-02-10 18:05:45 +0100245 def _onchange_conserve_write(self, param_name, old, new):
246 if self.rs:
247 self.rs.conserve_write = new
Harald Welte7829d8a2021-04-10 11:28:53 +0200248
Harald Weltec91085e2022-02-10 18:05:45 +0100249 def _onchange_apdu_trace(self, param_name, old, new):
250 if self.card:
251 if new == True:
252 self.card._scc._tp.apdu_tracer = self.Cmd2ApduTracer(self)
253 else:
254 self.card._scc._tp.apdu_tracer = None
Harald Welte7829d8a2021-04-10 11:28:53 +0200255
Harald Weltec91085e2022-02-10 18:05:45 +0100256 class Cmd2ApduTracer(ApduTracer):
257 def __init__(self, cmd2_app):
258 self.cmd2 = app
Harald Welte7829d8a2021-04-10 11:28:53 +0200259
Harald Weltec91085e2022-02-10 18:05:45 +0100260 def trace_response(self, cmd, sw, resp):
261 self.cmd2.poutput("-> %s %s" % (cmd[:10], cmd[10:]))
262 self.cmd2.poutput("<- %s: %s" % (sw, resp))
Harald Welteb2edd142021-01-08 23:29:35 +0100263
Harald Weltec91085e2022-02-10 18:05:45 +0100264 def update_prompt(self):
Harald Weltea6c0f882022-07-17 14:23:17 +0200265 if self.lchan:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200266 path_str = self.lchan.selected_file.fully_qualified_path_str(not self.numeric_path)
267 self.prompt = 'pySIM-shell (%s)> ' % (path_str)
Harald Weltec91085e2022-02-10 18:05:45 +0100268 else:
Philipp Maier7226c092022-06-01 17:58:38 +0200269 if self.card:
270 self.prompt = 'pySIM-shell (no card profile)> '
271 else:
272 self.prompt = 'pySIM-shell (no card)> '
Harald Welteb2edd142021-01-08 23:29:35 +0100273
Harald Weltec91085e2022-02-10 18:05:45 +0100274 @cmd2.with_category(CUSTOM_CATEGORY)
275 def do_intro(self, _):
276 """Display the intro banner"""
277 self.poutput(self.intro)
Philipp Maier9764de22021-11-03 10:44:39 +0100278
Harald Weltec91085e2022-02-10 18:05:45 +0100279 def do_eof(self, _: argparse.Namespace) -> bool:
280 self.poutput("")
281 return self.do_quit('')
Philipp Maier5d698e52021-09-16 13:18:01 +0200282
Harald Weltec91085e2022-02-10 18:05:45 +0100283 @cmd2.with_category(CUSTOM_CATEGORY)
284 def do_equip(self, opts):
285 """Equip pySim-shell with card"""
286 rs, card = init_card(sl)
287 self.equip(card, rs)
Philipp Maier76667642021-09-22 16:53:22 +0200288
Philipp Maier7226c092022-06-01 17:58:38 +0200289 apdu_cmd_parser = argparse.ArgumentParser()
290 apdu_cmd_parser.add_argument('APDU', type=str, help='APDU as hex string')
Philipp Maiere7d1b672022-06-01 18:05:34 +0200291 apdu_cmd_parser.add_argument('--expect-sw', help='expect a specified status word', type=str, default=None)
Philipp Maier7226c092022-06-01 17:58:38 +0200292
293 @cmd2.with_argparser(apdu_cmd_parser)
294 def do_apdu(self, opts):
295 """Send a raw APDU to the card, and print SW + Response.
296 DANGEROUS: pySim-shell will not know any card state changes, and
297 not continue to work as expected if you e.g. select a different
298 file."""
299 data, sw = self.card._scc._tp.send_apdu(opts.APDU)
300 if data:
301 self.poutput("SW: %s, RESP: %s" % (sw, data))
302 else:
303 self.poutput("SW: %s" % sw)
Philipp Maiere7d1b672022-06-01 18:05:34 +0200304 if opts.expect_sw:
305 if not sw_match(sw, opts.expect_sw):
306 raise SwMatchError(sw, opts.expect_sw)
Philipp Maier7226c092022-06-01 17:58:38 +0200307
Harald Weltec91085e2022-02-10 18:05:45 +0100308 class InterceptStderr(list):
309 def __init__(self):
310 self._stderr_backup = sys.stderr
Philipp Maier76667642021-09-22 16:53:22 +0200311
Harald Weltec91085e2022-02-10 18:05:45 +0100312 def __enter__(self):
313 self._stringio_stderr = StringIO()
314 sys.stderr = self._stringio_stderr
315 return self
Philipp Maier76667642021-09-22 16:53:22 +0200316
Harald Weltec91085e2022-02-10 18:05:45 +0100317 def __exit__(self, *args):
318 self.stderr = self._stringio_stderr.getvalue().strip()
319 del self._stringio_stderr
320 sys.stderr = self._stderr_backup
Philipp Maier76667642021-09-22 16:53:22 +0200321
Harald Weltec91085e2022-02-10 18:05:45 +0100322 def _show_failure_sign(self):
Harald Welte961b8032023-04-27 17:30:22 +0200323 self.poutput(style(" +-------------+", fg=LIGHT_RED))
324 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
325 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
326 self.poutput(style(" + ### +", fg=LIGHT_RED))
327 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
328 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
329 self.poutput(style(" +-------------+", fg=LIGHT_RED))
Harald Weltec91085e2022-02-10 18:05:45 +0100330 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200331
Harald Weltec91085e2022-02-10 18:05:45 +0100332 def _show_success_sign(self):
Harald Welte961b8032023-04-27 17:30:22 +0200333 self.poutput(style(" +-------------+", fg=LIGHT_GREEN))
334 self.poutput(style(" + ## +", fg=LIGHT_GREEN))
335 self.poutput(style(" + ## +", fg=LIGHT_GREEN))
336 self.poutput(style(" + # ## +", fg=LIGHT_GREEN))
337 self.poutput(style(" + ## # +", fg=LIGHT_GREEN))
338 self.poutput(style(" + ## +", fg=LIGHT_GREEN))
339 self.poutput(style(" +-------------+", fg=LIGHT_GREEN))
Harald Weltec91085e2022-02-10 18:05:45 +0100340 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200341
Harald Weltec91085e2022-02-10 18:05:45 +0100342 def _process_card(self, first, script_path):
Philipp Maier76667642021-09-22 16:53:22 +0200343
Harald Weltec91085e2022-02-10 18:05:45 +0100344 # Early phase of card initialzation (this part may fail with an exception)
345 try:
346 rs, card = init_card(self.sl)
347 rc = self.equip(card, rs)
348 except:
349 self.poutput("")
350 self.poutput("Card initialization failed with an exception:")
351 self.poutput("---------------------8<---------------------")
352 traceback.print_exc()
353 self.poutput("---------------------8<---------------------")
354 self.poutput("")
355 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200356
Harald Weltec91085e2022-02-10 18:05:45 +0100357 # Actual card processing step. This part should never fail with an exception since the cmd2
358 # do_run_script method will catch any exception that might occur during script execution.
359 if rc:
360 self.poutput("")
361 self.poutput("Transcript stdout:")
362 self.poutput("---------------------8<---------------------")
363 with self.InterceptStderr() as logged:
364 self.do_run_script(script_path)
365 self.poutput("---------------------8<---------------------")
Philipp Maier76667642021-09-22 16:53:22 +0200366
Harald Weltec91085e2022-02-10 18:05:45 +0100367 self.poutput("")
368 self.poutput("Transcript stderr:")
369 if logged.stderr:
370 self.poutput("---------------------8<---------------------")
371 self.poutput(logged.stderr)
372 self.poutput("---------------------8<---------------------")
373 else:
374 self.poutput("(none)")
Philipp Maier76667642021-09-22 16:53:22 +0200375
Harald Weltec91085e2022-02-10 18:05:45 +0100376 # Check for exceptions
377 self.poutput("")
378 if "EXCEPTION of type" not in logged.stderr:
379 return 0
Philipp Maier76667642021-09-22 16:53:22 +0200380
Harald Weltec91085e2022-02-10 18:05:45 +0100381 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200382
Harald Weltec91085e2022-02-10 18:05:45 +0100383 bulk_script_parser = argparse.ArgumentParser()
384 bulk_script_parser.add_argument(
385 'script_path', help="path to the script file")
386 bulk_script_parser.add_argument('--halt_on_error', help='stop card handling if an exeption occurs',
387 action='store_true')
388 bulk_script_parser.add_argument('--tries', type=int, default=2,
389 help='how many tries before trying the next card')
390 bulk_script_parser.add_argument('--on_stop_action', type=str, default=None,
391 help='commandline to execute when card handling has stopped')
392 bulk_script_parser.add_argument('--pre_card_action', type=str, default=None,
393 help='commandline to execute before actually talking to the card')
Philipp Maier76667642021-09-22 16:53:22 +0200394
Harald Weltec91085e2022-02-10 18:05:45 +0100395 @cmd2.with_argparser(bulk_script_parser)
396 @cmd2.with_category(CUSTOM_CATEGORY)
397 def do_bulk_script(self, opts):
398 """Run script on multiple cards (bulk provisioning)"""
Philipp Maier76667642021-09-22 16:53:22 +0200399
Harald Weltec91085e2022-02-10 18:05:45 +0100400 # Make sure that the script file exists and that it is readable.
401 if not os.access(opts.script_path, os.R_OK):
402 self.poutput("Invalid script file!")
403 return
Philipp Maier76667642021-09-22 16:53:22 +0200404
Harald Weltec91085e2022-02-10 18:05:45 +0100405 success_count = 0
406 fail_count = 0
Philipp Maier76667642021-09-22 16:53:22 +0200407
Harald Weltec91085e2022-02-10 18:05:45 +0100408 first = True
409 while 1:
410 # TODO: Count consecutive failures, if more than N consecutive failures occur, then stop.
411 # The ratinale is: There may be a problem with the device, we do want to prevent that
412 # all remaining cards are fired to the error bin. This is only relevant for situations
413 # with large stacks, probably we do not need this feature right now.
Philipp Maier76667642021-09-22 16:53:22 +0200414
Harald Weltec91085e2022-02-10 18:05:45 +0100415 try:
416 # In case of failure, try multiple times.
417 for i in range(opts.tries):
418 # fetch card into reader bay
419 ch.get(first)
Philipp Maier76667642021-09-22 16:53:22 +0200420
Harald Weltec91085e2022-02-10 18:05:45 +0100421 # if necessary execute an action before we start processing the card
422 if(opts.pre_card_action):
423 os.system(opts.pre_card_action)
Philipp Maier76667642021-09-22 16:53:22 +0200424
Harald Weltec91085e2022-02-10 18:05:45 +0100425 # process the card
426 rc = self._process_card(first, opts.script_path)
427 if rc == 0:
428 success_count = success_count + 1
429 self._show_success_sign()
430 self.poutput("Statistics: success :%i, failure: %i" % (
431 success_count, fail_count))
432 break
433 else:
434 fail_count = fail_count + 1
435 self._show_failure_sign()
436 self.poutput("Statistics: success :%i, failure: %i" % (
437 success_count, fail_count))
Philipp Maier76667642021-09-22 16:53:22 +0200438
Harald Weltec91085e2022-02-10 18:05:45 +0100439 # Depending on success or failure, the card goes either in the "error" bin or in the
440 # "done" bin.
441 if rc < 0:
442 ch.error()
443 else:
444 ch.done()
Philipp Maier76667642021-09-22 16:53:22 +0200445
Harald Weltec91085e2022-02-10 18:05:45 +0100446 # In most cases it is possible to proceed with the next card, but the
447 # user may decide to halt immediately when an error occurs
448 if opts.halt_on_error and rc < 0:
449 return
Philipp Maier76667642021-09-22 16:53:22 +0200450
Harald Weltec91085e2022-02-10 18:05:45 +0100451 except (KeyboardInterrupt):
452 self.poutput("")
453 self.poutput("Terminated by user!")
454 return
455 except (SystemExit):
456 # When all cards are processed the card handler device will throw a SystemExit
457 # exception. Also Errors that are not recoverable (cards stuck etc.) will end up here.
458 # The user has the option to execute some action to make aware that the card handler
459 # needs service.
460 if(opts.on_stop_action):
461 os.system(opts.on_stop_action)
462 return
463 except:
464 self.poutput("")
465 self.poutput("Card handling failed with an exception:")
466 self.poutput("---------------------8<---------------------")
467 traceback.print_exc()
468 self.poutput("---------------------8<---------------------")
469 self.poutput("")
470 fail_count = fail_count + 1
471 self._show_failure_sign()
472 self.poutput("Statistics: success :%i, failure: %i" %
473 (success_count, fail_count))
Philipp Maierb52feed2021-09-22 16:43:13 +0200474
Harald Weltec91085e2022-02-10 18:05:45 +0100475 first = False
476
477 echo_parser = argparse.ArgumentParser()
478 echo_parser.add_argument('string', help="string to echo on the shell")
479
480 @cmd2.with_argparser(echo_parser)
481 @cmd2.with_category(CUSTOM_CATEGORY)
482 def do_echo(self, opts):
483 """Echo (print) a string on the console"""
484 self.poutput(opts.string)
485
Harald Weltefc315482022-07-23 12:49:14 +0200486 @cmd2.with_category(CUSTOM_CATEGORY)
487 def do_version(self, opts):
488 """Print the pySim software version."""
489 import pkg_resources
490 self.poutput(pkg_resources.get_distribution('pySim'))
Harald Welteb2edd142021-01-08 23:29:35 +0100491
Harald Welte31d2cf02021-04-03 10:47:29 +0200492@with_default_category('pySim Commands')
493class PySimCommands(CommandSet):
Harald Weltec91085e2022-02-10 18:05:45 +0100494 def __init__(self):
495 super().__init__()
Harald Welteb2edd142021-01-08 23:29:35 +0100496
Harald Weltec91085e2022-02-10 18:05:45 +0100497 dir_parser = argparse.ArgumentParser()
498 dir_parser.add_argument(
499 '--fids', help='Show file identifiers', action='store_true')
500 dir_parser.add_argument(
501 '--names', help='Show file names', action='store_true')
502 dir_parser.add_argument(
503 '--apps', help='Show applications', action='store_true')
504 dir_parser.add_argument(
505 '--all', help='Show all selectable identifiers and names', action='store_true')
Philipp Maier5d3e2592021-02-22 17:22:16 +0100506
Harald Weltec91085e2022-02-10 18:05:45 +0100507 @cmd2.with_argparser(dir_parser)
508 def do_dir(self, opts):
509 """Show a listing of files available in currently selected DF or MF"""
510 if opts.all:
511 flags = []
512 elif opts.fids or opts.names or opts.apps:
513 flags = ['PARENT', 'SELF']
514 if opts.fids:
515 flags += ['FIDS', 'AIDS']
516 if opts.names:
517 flags += ['FNAMES', 'ANAMES']
518 if opts.apps:
519 flags += ['ANAMES', 'AIDS']
520 else:
521 flags = ['PARENT', 'SELF', 'FNAMES', 'ANAMES']
522 selectables = list(
Harald Weltea6c0f882022-07-17 14:23:17 +0200523 self._cmd.lchan.selected_file.get_selectable_names(flags=flags))
Harald Weltec91085e2022-02-10 18:05:45 +0100524 directory_str = tabulate_str_list(
525 selectables, width=79, hspace=2, lspace=1, align_left=True)
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200526 path = self._cmd.lchan.selected_file.fully_qualified_path_str(True)
527 self._cmd.poutput(path)
528 path = self._cmd.lchan.selected_file.fully_qualified_path_str(False)
529 self._cmd.poutput(path)
Harald Weltec91085e2022-02-10 18:05:45 +0100530 self._cmd.poutput(directory_str)
531 self._cmd.poutput("%d files" % len(selectables))
Harald Welteb2edd142021-01-08 23:29:35 +0100532
Philipp Maier7b138b02022-05-31 13:42:56 +0200533 def walk(self, indent=0, action_ef=None, action_df=None, context=None, **kwargs):
Harald Weltec91085e2022-02-10 18:05:45 +0100534 """Recursively walk through the file system, starting at the currently selected DF"""
Philipp Maier7b138b02022-05-31 13:42:56 +0200535
Harald Weltea6c0f882022-07-17 14:23:17 +0200536 if isinstance(self._cmd.lchan.selected_file, CardDF):
Philipp Maier7b138b02022-05-31 13:42:56 +0200537 if action_df:
538 action_df(context, opts)
539
Harald Weltea6c0f882022-07-17 14:23:17 +0200540 files = self._cmd.lchan.selected_file.get_selectables(
Harald Weltec91085e2022-02-10 18:05:45 +0100541 flags=['FNAMES', 'ANAMES'])
542 for f in files:
Philipp Maier7b138b02022-05-31 13:42:56 +0200543 # special case: When no action is performed, just output a directory
544 if not action_ef and not action_df:
Harald Weltec91085e2022-02-10 18:05:45 +0100545 output_str = " " * indent + str(f) + (" " * 250)
546 output_str = output_str[0:25]
547 if isinstance(files[f], CardADF):
548 output_str += " " + str(files[f].aid)
549 else:
550 output_str += " " + str(files[f].fid)
551 output_str += " " + str(files[f].desc)
552 self._cmd.poutput(output_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200553
Harald Weltec91085e2022-02-10 18:05:45 +0100554 if isinstance(files[f], CardDF):
555 skip_df = False
556 try:
Harald Weltea6c0f882022-07-17 14:23:17 +0200557 fcp_dec = self._cmd.lchan.select(f, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100558 except Exception as e:
559 skip_df = True
Harald Weltea6c0f882022-07-17 14:23:17 +0200560 df = self._cmd.lchan.selected_file
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200561 df_path = df.fully_qualified_path_str(True)
562 df_skip_reason_str = df_path + \
Harald Weltec91085e2022-02-10 18:05:45 +0100563 "/" + str(f) + ", " + str(e)
564 if context:
565 context['DF_SKIP'] += 1
566 context['DF_SKIP_REASON'].append(df_skip_reason_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200567
Harald Weltec91085e2022-02-10 18:05:45 +0100568 # If the DF was skipped, we never have entered the directory
569 # below, so we must not move up.
570 if skip_df == False:
Philipp Maier7b138b02022-05-31 13:42:56 +0200571 self.walk(indent + 1, action_ef, action_df, context, **kwargs)
Harald Weltea6c0f882022-07-17 14:23:17 +0200572 fcp_dec = self._cmd.lchan.select("..", self._cmd)
Philipp Maierf408a402021-04-09 21:16:12 +0200573
Philipp Maier7b138b02022-05-31 13:42:56 +0200574 elif action_ef:
Harald Weltea6c0f882022-07-17 14:23:17 +0200575 df_before_action = self._cmd.lchan.selected_file
Philipp Maier7b138b02022-05-31 13:42:56 +0200576 action_ef(f, context, **kwargs)
Harald Weltec91085e2022-02-10 18:05:45 +0100577 # When walking through the file system tree the action must not
578 # always restore the currently selected file to the file that
579 # was selected before executing the action() callback.
Harald Weltea6c0f882022-07-17 14:23:17 +0200580 if df_before_action != self._cmd.lchan.selected_file:
Harald Weltec91085e2022-02-10 18:05:45 +0100581 raise RuntimeError("inconsistent walk, %s is currently selected but expecting %s to be selected"
Harald Weltea6c0f882022-07-17 14:23:17 +0200582 % (str(self._cmd.lchan.selected_file), str(df_before_action)))
Philipp Maierff9dae22021-02-25 17:03:21 +0100583
Harald Weltec91085e2022-02-10 18:05:45 +0100584 def do_tree(self, opts):
585 """Display a filesystem-tree with all selectable files"""
586 self.walk()
Philipp Maierff9dae22021-02-25 17:03:21 +0100587
Philipp Maier7b138b02022-05-31 13:42:56 +0200588 def export_ef(self, filename, context, as_json):
589 """ Select and export a single elementary file (EF) """
Harald Weltec91085e2022-02-10 18:05:45 +0100590 context['COUNT'] += 1
Harald Weltea6c0f882022-07-17 14:23:17 +0200591 df = self._cmd.lchan.selected_file
Philipp Maierac34dcc2021-04-01 17:19:05 +0200592
Philipp Maierea81f752022-05-19 10:13:30 +0200593 # The currently selected file (not the file we are going to export)
594 # must always be an ADF or DF. From this starting point we select
595 # the EF we want to export. To maintain consistency we will then
596 # select the current DF again (see comment below).
Harald Weltec91085e2022-02-10 18:05:45 +0100597 if not isinstance(df, CardDF):
598 raise RuntimeError(
599 "currently selected file %s is not a DF or ADF" % str(df))
Philipp Maierac34dcc2021-04-01 17:19:05 +0200600
Harald Weltec91085e2022-02-10 18:05:45 +0100601 df_path_list = df.fully_qualified_path(True)
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200602 df_path = df.fully_qualified_path_str(True)
603 df_path_fid = df.fully_qualified_path_str(False)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100604
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200605 file_str = df_path + "/" + str(filename)
Harald Weltec91085e2022-02-10 18:05:45 +0100606 self._cmd.poutput(boxed_heading_str(file_str))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100607
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200608 self._cmd.poutput("# directory: %s (%s)" % (df_path, df_path_fid))
Harald Weltec91085e2022-02-10 18:05:45 +0100609 try:
Harald Weltea6c0f882022-07-17 14:23:17 +0200610 fcp_dec = self._cmd.lchan.select(filename, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100611 self._cmd.poutput("# file: %s (%s)" % (
Harald Weltea6c0f882022-07-17 14:23:17 +0200612 self._cmd.lchan.selected_file.name, self._cmd.lchan.selected_file.fid))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100613
Harald Weltea6c0f882022-07-17 14:23:17 +0200614 structure = self._cmd.lchan.selected_file_structure()
Harald Weltec91085e2022-02-10 18:05:45 +0100615 self._cmd.poutput("# structure: %s" % str(structure))
Harald Weltea6c0f882022-07-17 14:23:17 +0200616 self._cmd.poutput("# RAW FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp_hex))
617 self._cmd.poutput("# Decoded FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100618
Harald Weltec91085e2022-02-10 18:05:45 +0100619 for f in df_path_list:
620 self._cmd.poutput("select " + str(f))
Harald Weltea6c0f882022-07-17 14:23:17 +0200621 self._cmd.poutput("select " + self._cmd.lchan.selected_file.name)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100622
Harald Weltec91085e2022-02-10 18:05:45 +0100623 if structure == 'transparent':
Harald Welte08b11ab2022-02-10 18:56:41 +0100624 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200625 result = self._cmd.lchan.read_binary_dec()
Harald Welte08b11ab2022-02-10 18:56:41 +0100626 self._cmd.poutput("update_binary_decoded '%s'" % json.dumps(result[0], cls=JsonEncoder))
627 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200628 result = self._cmd.lchan.read_binary()
Harald Welte08b11ab2022-02-10 18:56:41 +0100629 self._cmd.poutput("update_binary " + str(result[0]))
Harald Weltec91085e2022-02-10 18:05:45 +0100630 elif structure == 'cyclic' or structure == 'linear_fixed':
631 # Use number of records specified in select response
Harald Weltea6c0f882022-07-17 14:23:17 +0200632 num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
Harald Welte747a9782022-02-13 17:52:28 +0100633 if num_of_rec:
Harald Weltec91085e2022-02-10 18:05:45 +0100634 for r in range(1, num_of_rec + 1):
Harald Welte08b11ab2022-02-10 18:56:41 +0100635 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200636 result = self._cmd.lchan.read_record_dec(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100637 self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
638 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200639 result = self._cmd.lchan.read_record(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100640 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
641
Harald Weltec91085e2022-02-10 18:05:45 +0100642 # When the select response does not return the number of records, read until we hit the
643 # first record that cannot be read.
644 else:
645 r = 1
646 while True:
647 try:
Harald Welte08b11ab2022-02-10 18:56:41 +0100648 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200649 result = self._cmd.lchan.read_record_dec(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100650 self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
651 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200652 result = self._cmd.lchan.read_record(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100653 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
Harald Weltec91085e2022-02-10 18:05:45 +0100654 except SwMatchError as e:
655 # We are past the last valid record - stop
656 if e.sw_actual == "9402":
657 break
658 # Some other problem occurred
659 else:
660 raise e
Harald Weltec91085e2022-02-10 18:05:45 +0100661 r = r + 1
662 elif structure == 'ber_tlv':
Harald Weltea6c0f882022-07-17 14:23:17 +0200663 tags = self._cmd.lchan.retrieve_tags()
Harald Weltec91085e2022-02-10 18:05:45 +0100664 for t in tags:
Harald Weltea6c0f882022-07-17 14:23:17 +0200665 result = self._cmd.lchan.retrieve_data(t)
Harald Weltec91085e2022-02-10 18:05:45 +0100666 (tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
667 self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val)))
668 else:
669 raise RuntimeError(
670 'Unsupported structure "%s" of file "%s"' % (structure, filename))
671 except Exception as e:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200672 bad_file_str = df_path + "/" + str(filename) + ", " + str(e)
Harald Weltec91085e2022-02-10 18:05:45 +0100673 self._cmd.poutput("# bad file: %s" % bad_file_str)
674 context['ERR'] += 1
675 context['BAD'].append(bad_file_str)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100676
Harald Weltec91085e2022-02-10 18:05:45 +0100677 # When reading the file is done, make sure the parent file is
678 # selected again. This will be the usual case, however we need
679 # to check before since we must not select the same DF twice
Harald Weltea6c0f882022-07-17 14:23:17 +0200680 if df != self._cmd.lchan.selected_file:
681 self._cmd.lchan.select(df.fid or df.aid, self._cmd)
Philipp Maierac34dcc2021-04-01 17:19:05 +0200682
Harald Weltec91085e2022-02-10 18:05:45 +0100683 self._cmd.poutput("#")
Philipp Maier24f7bd32021-02-25 17:06:18 +0100684
Harald Weltec91085e2022-02-10 18:05:45 +0100685 export_parser = argparse.ArgumentParser()
686 export_parser.add_argument(
687 '--filename', type=str, default=None, help='only export specific file')
Harald Welte08b11ab2022-02-10 18:56:41 +0100688 export_parser.add_argument(
689 '--json', action='store_true', help='export as JSON (less reliable)')
Philipp Maier24f7bd32021-02-25 17:06:18 +0100690
Harald Weltec91085e2022-02-10 18:05:45 +0100691 @cmd2.with_argparser(export_parser)
692 def do_export(self, opts):
693 """Export files to script that can be imported back later"""
694 context = {'ERR': 0, 'COUNT': 0, 'BAD': [],
695 'DF_SKIP': 0, 'DF_SKIP_REASON': []}
Philipp Maier9a4091d2022-05-19 10:20:30 +0200696 kwargs_export = {'as_json': opts.json}
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200697 exception_str_add = ""
698
Harald Weltec91085e2022-02-10 18:05:45 +0100699 if opts.filename:
Philipp Maier7b138b02022-05-31 13:42:56 +0200700 self.export_ef(opts.filename, context, **kwargs_export)
Harald Weltec91085e2022-02-10 18:05:45 +0100701 else:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200702 try:
703 self.walk(0, self.export_ef, None, context, **kwargs_export)
704 except Exception as e:
705 print("# Stopping early here due to exception: " + str(e))
706 print("#")
707 exception_str_add = ", also had to stop early due to exception:" + str(e)
Philipp Maier80ce71f2021-04-19 21:24:23 +0200708
Harald Weltec91085e2022-02-10 18:05:45 +0100709 self._cmd.poutput(boxed_heading_str("Export summary"))
Philipp Maier80ce71f2021-04-19 21:24:23 +0200710
Harald Weltec91085e2022-02-10 18:05:45 +0100711 self._cmd.poutput("# total files visited: %u" % context['COUNT'])
712 self._cmd.poutput("# bad files: %u" % context['ERR'])
713 for b in context['BAD']:
714 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200715
Harald Weltec91085e2022-02-10 18:05:45 +0100716 self._cmd.poutput("# skipped dedicated files(s): %u" %
717 context['DF_SKIP'])
718 for b in context['DF_SKIP_REASON']:
719 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200720
Harald Weltec91085e2022-02-10 18:05:45 +0100721 if context['ERR'] and context['DF_SKIP']:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200722 raise RuntimeError("unable to export %i elementary file(s) and %i dedicated file(s)%s" % (
723 context['ERR'], context['DF_SKIP'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100724 elif context['ERR']:
725 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200726 "unable to export %i elementary file(s)%s" % (context['ERR'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100727 elif context['DF_SKIP']:
728 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200729 "unable to export %i dedicated files(s)%s" % (context['ERR'], exception_str_add))
Harald Welteb2edd142021-01-08 23:29:35 +0100730
Harald Weltec91085e2022-02-10 18:05:45 +0100731 def do_reset(self, opts):
732 """Reset the Card."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200733 atr = self._cmd.lchan.reset(self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100734 self._cmd.poutput('Card ATR: %s' % atr)
735 self._cmd.update_prompt()
Harald Weltedaf2b392021-05-03 23:17:29 +0200736
Harald Weltec91085e2022-02-10 18:05:45 +0100737 def do_desc(self, opts):
738 """Display human readable file description for the currently selected file"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200739 desc = self._cmd.lchan.selected_file.desc
Harald Weltec91085e2022-02-10 18:05:45 +0100740 if desc:
741 self._cmd.poutput(desc)
742 else:
743 self._cmd.poutput("no description available")
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200744
Harald Weltec91085e2022-02-10 18:05:45 +0100745 def do_verify_adm(self, arg):
746 """VERIFY the ADM1 PIN"""
747 if arg:
748 # use specified ADM-PIN
749 pin_adm = sanitize_pin_adm(arg)
750 else:
751 # try to find an ADM-PIN if none is specified
752 result = card_key_provider_get_field(
753 'ADM1', key='ICCID', value=self._cmd.iccid)
754 pin_adm = sanitize_pin_adm(result)
755 if pin_adm:
756 self._cmd.poutput(
757 "found ADM-PIN '%s' for ICCID '%s'" % (result, self._cmd.iccid))
758 else:
759 raise ValueError(
760 "cannot find ADM-PIN for ICCID '%s'" % (self._cmd.iccid))
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200761
Harald Weltec91085e2022-02-10 18:05:45 +0100762 if pin_adm:
763 self._cmd.card.verify_adm(h2b(pin_adm))
764 else:
765 raise ValueError("error: cannot authenticate, no adm-pin!")
766
Philipp Maier7b9e2442023-03-22 15:19:54 +0100767 def do_cardinfo(self, opts):
768 """Display information about the currently inserted card"""
769 self._cmd.poutput("Card info:")
770 self._cmd.poutput(" Name: %s" % self._cmd.card.name)
771 self._cmd.poutput(" ATR: %s" % b2h(self._cmd.card._scc.get_atr()))
772 self._cmd.poutput(" ICCID: %s" % self._cmd.iccid)
773 self._cmd.poutput(" Class-Byte: %s" % self._cmd.card._scc.cla_byte)
774 self._cmd.poutput(" Select-Ctrl: %s" % self._cmd.card._scc.sel_ctrl)
775 self._cmd.poutput(" AIDs:")
776 for a in self._cmd.rs.mf.applications:
777 self._cmd.poutput(" %s" % a)
Harald Welteb2edd142021-01-08 23:29:35 +0100778
Harald Welte31d2cf02021-04-03 10:47:29 +0200779@with_default_category('ISO7816 Commands')
780class Iso7816Commands(CommandSet):
Harald Weltec91085e2022-02-10 18:05:45 +0100781 def __init__(self):
782 super().__init__()
Harald Welte31d2cf02021-04-03 10:47:29 +0200783
Harald Weltec91085e2022-02-10 18:05:45 +0100784 def do_select(self, opts):
785 """SELECT a File (ADF/DF/EF)"""
786 if len(opts.arg_list) == 0:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200787 path = self._cmd.lchan.selected_file.fully_qualified_path_str(True)
788 path_fid = self._cmd.lchan.selected_file.fully_qualified_path_str(False)
789 self._cmd.poutput("currently selected file: %s (%s)" % (path, path_fid))
Harald Weltec91085e2022-02-10 18:05:45 +0100790 return
Harald Welte31d2cf02021-04-03 10:47:29 +0200791
Harald Weltec91085e2022-02-10 18:05:45 +0100792 path = opts.arg_list[0]
Harald Weltea6c0f882022-07-17 14:23:17 +0200793 fcp_dec = self._cmd.lchan.select(path, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100794 self._cmd.update_prompt()
795 self._cmd.poutput_json(fcp_dec)
Harald Welte31d2cf02021-04-03 10:47:29 +0200796
Harald Weltec91085e2022-02-10 18:05:45 +0100797 def complete_select(self, text, line, begidx, endidx) -> List[str]:
798 """Command Line tab completion for SELECT"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200799 index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
Harald Weltec91085e2022-02-10 18:05:45 +0100800 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200801
Harald Weltec91085e2022-02-10 18:05:45 +0100802 def get_code(self, code):
803 """Use code either directly or try to get it from external data source"""
804 auto = ('PIN1', 'PIN2', 'PUK1', 'PUK2')
Harald Welte31d2cf02021-04-03 10:47:29 +0200805
Harald Weltec91085e2022-02-10 18:05:45 +0100806 if str(code).upper() not in auto:
807 return sanitize_pin_adm(code)
Harald Welte31d2cf02021-04-03 10:47:29 +0200808
Harald Weltec91085e2022-02-10 18:05:45 +0100809 result = card_key_provider_get_field(
810 str(code), key='ICCID', value=self._cmd.iccid)
811 result = sanitize_pin_adm(result)
812 if result:
813 self._cmd.poutput("found %s '%s' for ICCID '%s'" %
814 (code.upper(), result, self._cmd.iccid))
815 else:
816 self._cmd.poutput("cannot find %s for ICCID '%s'" %
817 (code.upper(), self._cmd.iccid))
818 return result
Harald Welte31d2cf02021-04-03 10:47:29 +0200819
Harald Weltec91085e2022-02-10 18:05:45 +0100820 verify_chv_parser = argparse.ArgumentParser()
821 verify_chv_parser.add_argument(
822 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
823 verify_chv_parser.add_argument(
824 'pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
Harald Welte31d2cf02021-04-03 10:47:29 +0200825
Harald Weltec91085e2022-02-10 18:05:45 +0100826 @cmd2.with_argparser(verify_chv_parser)
827 def do_verify_chv(self, opts):
Harald Welte12af7932022-02-15 16:39:08 +0100828 """Verify (authenticate) using specified CHV (PIN) code, which is how the specifications
829 call it if you authenticate yourself using the specified PIN. There usually is at least PIN1 and
830 PIN2."""
Harald Weltec91085e2022-02-10 18:05:45 +0100831 pin = self.get_code(opts.pin_code)
832 (data, sw) = self._cmd.card._scc.verify_chv(opts.pin_nr, h2b(pin))
833 self._cmd.poutput("CHV verification successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200834
Harald Weltec91085e2022-02-10 18:05:45 +0100835 unblock_chv_parser = argparse.ArgumentParser()
836 unblock_chv_parser.add_argument(
837 '--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
838 unblock_chv_parser.add_argument(
839 'puk_code', type=str, help='PUK code digits \"PUK1\" or \"PUK2\" to get PUK code from external data source')
840 unblock_chv_parser.add_argument(
841 'new_pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
Harald Welte31d2cf02021-04-03 10:47:29 +0200842
Harald Weltec91085e2022-02-10 18:05:45 +0100843 @cmd2.with_argparser(unblock_chv_parser)
844 def do_unblock_chv(self, opts):
845 """Unblock PIN code using specified PUK code"""
846 new_pin = self.get_code(opts.new_pin_code)
847 puk = self.get_code(opts.puk_code)
848 (data, sw) = self._cmd.card._scc.unblock_chv(
849 opts.pin_nr, h2b(puk), h2b(new_pin))
850 self._cmd.poutput("CHV unblock successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200851
Harald Weltec91085e2022-02-10 18:05:45 +0100852 change_chv_parser = argparse.ArgumentParser()
853 change_chv_parser.add_argument(
854 '--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
855 change_chv_parser.add_argument(
856 'pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
857 change_chv_parser.add_argument(
858 'new_pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
Harald Welte31d2cf02021-04-03 10:47:29 +0200859
Harald Weltec91085e2022-02-10 18:05:45 +0100860 @cmd2.with_argparser(change_chv_parser)
861 def do_change_chv(self, opts):
862 """Change PIN code to a new PIN code"""
863 new_pin = self.get_code(opts.new_pin_code)
864 pin = self.get_code(opts.pin_code)
865 (data, sw) = self._cmd.card._scc.change_chv(
866 opts.pin_nr, h2b(pin), h2b(new_pin))
867 self._cmd.poutput("CHV change successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200868
Harald Weltec91085e2022-02-10 18:05:45 +0100869 disable_chv_parser = argparse.ArgumentParser()
870 disable_chv_parser.add_argument(
871 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
872 disable_chv_parser.add_argument(
873 'pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
Harald Welte31d2cf02021-04-03 10:47:29 +0200874
Harald Weltec91085e2022-02-10 18:05:45 +0100875 @cmd2.with_argparser(disable_chv_parser)
876 def do_disable_chv(self, opts):
877 """Disable PIN code using specified PIN code"""
878 pin = self.get_code(opts.pin_code)
879 (data, sw) = self._cmd.card._scc.disable_chv(opts.pin_nr, h2b(pin))
880 self._cmd.poutput("CHV disable successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200881
Harald Weltec91085e2022-02-10 18:05:45 +0100882 enable_chv_parser = argparse.ArgumentParser()
883 enable_chv_parser.add_argument(
884 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
885 enable_chv_parser.add_argument(
886 'pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
Harald Welte31d2cf02021-04-03 10:47:29 +0200887
Harald Weltec91085e2022-02-10 18:05:45 +0100888 @cmd2.with_argparser(enable_chv_parser)
889 def do_enable_chv(self, opts):
890 """Enable PIN code using specified PIN code"""
891 pin = self.get_code(opts.pin_code)
892 (data, sw) = self._cmd.card._scc.enable_chv(opts.pin_nr, h2b(pin))
893 self._cmd.poutput("CHV enable successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200894
Harald Weltec91085e2022-02-10 18:05:45 +0100895 def do_deactivate_file(self, opts):
Harald Welte799c3542022-02-15 15:56:28 +0100896 """Deactivate the currently selected EF"""
Harald Weltec91085e2022-02-10 18:05:45 +0100897 (data, sw) = self._cmd.card._scc.deactivate_file()
Harald Weltea4631612021-04-10 18:17:55 +0200898
Harald Welte799c3542022-02-15 15:56:28 +0100899 activate_file_parser = argparse.ArgumentParser()
900 activate_file_parser.add_argument('NAME', type=str, help='File name or FID of file to activate')
901 @cmd2.with_argparser(activate_file_parser)
Harald Weltec91085e2022-02-10 18:05:45 +0100902 def do_activate_file(self, opts):
Harald Welte12af7932022-02-15 16:39:08 +0100903 """Activate the specified EF. This used to be called REHABILITATE in TS 11.11 for classic
904 SIM. You need to specify the name or FID of the file to activate."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200905 (data, sw) = self._cmd.lchan.activate_file(opts.NAME)
Harald Welte485692b2021-05-25 22:21:44 +0200906
Harald Weltec91085e2022-02-10 18:05:45 +0100907 def complete_activate_file(self, text, line, begidx, endidx) -> List[str]:
908 """Command Line tab completion for ACTIVATE FILE"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200909 index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
Harald Weltec91085e2022-02-10 18:05:45 +0100910 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200911
Harald Weltec91085e2022-02-10 18:05:45 +0100912 open_chan_parser = argparse.ArgumentParser()
913 open_chan_parser.add_argument(
914 'chan_nr', type=int, default=0, help='Channel Number')
Harald Welte703f9332021-04-10 18:39:32 +0200915
Harald Weltec91085e2022-02-10 18:05:45 +0100916 @cmd2.with_argparser(open_chan_parser)
917 def do_open_channel(self, opts):
918 """Open a logical channel."""
919 (data, sw) = self._cmd.card._scc.manage_channel(
920 mode='open', lchan_nr=opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200921
Harald Weltec91085e2022-02-10 18:05:45 +0100922 close_chan_parser = argparse.ArgumentParser()
923 close_chan_parser.add_argument(
924 'chan_nr', type=int, default=0, help='Channel Number')
Harald Welte703f9332021-04-10 18:39:32 +0200925
Harald Weltec91085e2022-02-10 18:05:45 +0100926 @cmd2.with_argparser(close_chan_parser)
927 def do_close_channel(self, opts):
928 """Close a logical channel."""
929 (data, sw) = self._cmd.card._scc.manage_channel(
930 mode='close', lchan_nr=opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200931
Harald Weltec91085e2022-02-10 18:05:45 +0100932 def do_status(self, opts):
933 """Perform the STATUS command."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200934 fcp_dec = self._cmd.lchan.status()
Harald Weltec91085e2022-02-10 18:05:45 +0100935 self._cmd.poutput_json(fcp_dec)
Harald Welte34b05d32021-05-25 22:03:13 +0200936
Harald Weltec91085e2022-02-10 18:05:45 +0100937 suspend_uicc_parser = argparse.ArgumentParser()
938 suspend_uicc_parser.add_argument('--min-duration-secs', type=int, default=60,
939 help='Proposed minimum duration of suspension')
940 suspend_uicc_parser.add_argument('--max-duration-secs', type=int, default=24*60*60,
941 help='Proposed maximum duration of suspension')
Harald Welteec950532021-10-20 13:09:00 +0200942
Harald Weltec91085e2022-02-10 18:05:45 +0100943 # not ISO7816-4 but TS 102 221
944 @cmd2.with_argparser(suspend_uicc_parser)
945 def do_suspend_uicc(self, opts):
946 """Perform the SUSPEND UICC command. Only supported on some UICC."""
947 (duration, token, sw) = self._cmd.card._scc.suspend_uicc(min_len_secs=opts.min_duration_secs,
948 max_len_secs=opts.max_duration_secs)
949 self._cmd.poutput(
950 'Negotiated Duration: %u secs, Token: %s, SW: %s' % (duration, token, sw))
Harald Welteec950532021-10-20 13:09:00 +0200951
Harald Weltecab26c72022-08-06 16:12:30 +0200952class Proact(ProactiveHandler):
953 def receive_fetch(self, pcmd: ProactiveCommand):
954 # print its parsed representation
955 print(pcmd.decoded)
956 # TODO: implement the basics, such as SMS Sending, ...
957
958
Harald Welte703f9332021-04-10 18:39:32 +0200959
Harald Weltef2e761c2021-04-11 11:56:44 +0200960option_parser = argparse.ArgumentParser(prog='pySim-shell', description='interactive SIM card shell',
961 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
Harald Welte28c24312021-04-11 12:19:36 +0200962argparse_add_reader_args(option_parser)
Harald Weltec8ff0262021-04-11 12:06:13 +0200963
964global_group = option_parser.add_argument_group('General Options')
965global_group.add_argument('--script', metavar='PATH', default=None,
Harald Welte28c24312021-04-11 12:19:36 +0200966 help='script with pySim-shell commands to be executed automatically at start-up')
Harald Weltec91085e2022-02-10 18:05:45 +0100967global_group.add_argument('--csv', metavar='FILE',
968 default=None, help='Read card data from CSV file')
Philipp Maier76667642021-09-22 16:53:22 +0200969global_group.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
Harald Weltec91085e2022-02-10 18:05:45 +0100970 help="Use automatic card handling machine")
Harald Weltec8ff0262021-04-11 12:06:13 +0200971
972adm_group = global_group.add_mutually_exclusive_group()
973adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
974 help='ADM PIN used for provisioning (overwrites default)')
975adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM1_HEX', dest='pin_adm_hex', default=None,
976 help='ADM PIN used for provisioning, as hex string (16 characters long)')
Harald Welteb2edd142021-01-08 23:29:35 +0100977
978
979if __name__ == '__main__':
980
Harald Weltec91085e2022-02-10 18:05:45 +0100981 # Parse options
982 opts = option_parser.parse_args()
Harald Welteb2edd142021-01-08 23:29:35 +0100983
Harald Weltec91085e2022-02-10 18:05:45 +0100984 # If a script file is specified, be sure that it actually exists
985 if opts.script:
986 if not os.access(opts.script, os.R_OK):
987 print("Invalid script file!")
988 sys.exit(2)
Philipp Maier13e258d2021-04-08 17:48:49 +0200989
Harald Weltec91085e2022-02-10 18:05:45 +0100990 # Register csv-file as card data provider, either from specified CSV
991 # or from CSV file in home directory
992 csv_default = str(Path.home()) + "/.osmocom/pysim/card_data.csv"
993 if opts.csv:
994 card_key_provider_register(CardKeyProviderCsv(opts.csv))
995 if os.path.isfile(csv_default):
996 card_key_provider_register(CardKeyProviderCsv(csv_default))
Philipp Maier2b11c322021-03-17 12:37:39 +0100997
Harald Weltec91085e2022-02-10 18:05:45 +0100998 # Init card reader driver
Harald Weltecab26c72022-08-06 16:12:30 +0200999 sl = init_reader(opts, proactive_handler = Proact())
Harald Weltec91085e2022-02-10 18:05:45 +01001000 if sl is None:
1001 exit(1)
Philipp Maierea95c392021-09-16 13:10:19 +02001002
Harald Weltec91085e2022-02-10 18:05:45 +01001003 # Create command layer
1004 scc = SimCardCommands(transport=sl)
Philipp Maierea95c392021-09-16 13:10:19 +02001005
Harald Weltec91085e2022-02-10 18:05:45 +01001006 # Create a card handler (for bulk provisioning)
1007 if opts.card_handler_config:
1008 ch = CardHandlerAuto(None, opts.card_handler_config)
1009 else:
1010 ch = CardHandler(sl)
Philipp Maier76667642021-09-22 16:53:22 +02001011
Harald Weltec91085e2022-02-10 18:05:45 +01001012 # Detect and initialize the card in the reader. This may fail when there
1013 # is no card in the reader or the card is unresponsive. PysimApp is
1014 # able to tolerate and recover from that.
1015 try:
1016 rs, card = init_card(sl)
1017 app = PysimApp(card, rs, sl, ch, opts.script)
1018 except:
1019 print("Card initialization failed with an exception:")
1020 print("---------------------8<---------------------")
1021 traceback.print_exc()
1022 print("---------------------8<---------------------")
1023 print("(you may still try to recover from this manually by using the 'equip' command.)")
1024 print(
1025 " it should also be noted that some readers may behave strangely when no card")
1026 print(" is inserted.)")
1027 print("")
Philipp Maier7226c092022-06-01 17:58:38 +02001028 app = PysimApp(card, None, sl, ch, opts.script)
Philipp Maierea95c392021-09-16 13:10:19 +02001029
Harald Weltec91085e2022-02-10 18:05:45 +01001030 # If the user supplies an ADM PIN at via commandline args authenticate
1031 # immediately so that the user does not have to use the shell commands
1032 pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
1033 if pin_adm:
1034 if not card:
1035 print("Card error, cannot do ADM verification with supplied ADM pin now.")
1036 try:
1037 card.verify_adm(h2b(pin_adm))
1038 except Exception as e:
1039 print(e)
Philipp Maier228c98e2021-03-10 20:14:06 +01001040
Harald Weltec91085e2022-02-10 18:05:45 +01001041 app.cmdloop()