blob: f6cd75d380389ddae65f48dcf99a5d3315a79998 [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
Philipp Maiere345e112023-06-09 15:19:56 +020053from pySim.utils import h2b, b2h, i2h, swap_nibbles, rpad, 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)
Harald Welte659781c2023-06-06 17:00:51 +0200222 if self.rs.profile:
223 for cmd_set in self.rs.profile.shell_cmdsets:
224 self.register_command_set(cmd_set)
Harald Weltec91085e2022-02-10 18:05:45 +0100225 self.register_command_set(Iso7816Commands())
Harald Welte3c9b7842021-10-19 21:44:24 +0200226 self.register_command_set(Ts102222Commands())
Harald Weltec91085e2022-02-10 18:05:45 +0100227 self.register_command_set(PySimCommands())
228 self.iccid, sw = self.card.read_iccid()
Harald Weltea6c0f882022-07-17 14:23:17 +0200229 self.lchan.select('MF', self)
Harald Weltec91085e2022-02-10 18:05:45 +0100230 rc = True
231 else:
232 self.poutput("pySim-shell not equipped!")
Philipp Maier5d698e52021-09-16 13:18:01 +0200233
Harald Weltec91085e2022-02-10 18:05:45 +0100234 self.update_prompt()
235 return rc
Harald Welteb2edd142021-01-08 23:29:35 +0100236
Harald Weltec91085e2022-02-10 18:05:45 +0100237 def poutput_json(self, data, force_no_pretty=False):
238 """like cmd2.poutput() but for a JSON serializable dict."""
239 if force_no_pretty or self.json_pretty_print == False:
240 output = json.dumps(data, cls=JsonEncoder)
241 else:
242 output = json.dumps(data, cls=JsonEncoder, indent=4)
243 self.poutput(output)
Harald Welteb2edd142021-01-08 23:29:35 +0100244
Harald Weltec91085e2022-02-10 18:05:45 +0100245 def _onchange_numeric_path(self, param_name, old, new):
246 self.update_prompt()
Philipp Maier38c74f62021-03-17 17:19:52 +0100247
Harald Weltec91085e2022-02-10 18:05:45 +0100248 def _onchange_conserve_write(self, param_name, old, new):
249 if self.rs:
250 self.rs.conserve_write = new
Harald Welte7829d8a2021-04-10 11:28:53 +0200251
Harald Weltec91085e2022-02-10 18:05:45 +0100252 def _onchange_apdu_trace(self, param_name, old, new):
253 if self.card:
254 if new == True:
255 self.card._scc._tp.apdu_tracer = self.Cmd2ApduTracer(self)
256 else:
257 self.card._scc._tp.apdu_tracer = None
Harald Welte7829d8a2021-04-10 11:28:53 +0200258
Harald Weltec91085e2022-02-10 18:05:45 +0100259 class Cmd2ApduTracer(ApduTracer):
260 def __init__(self, cmd2_app):
261 self.cmd2 = app
Harald Welte7829d8a2021-04-10 11:28:53 +0200262
Harald Weltec91085e2022-02-10 18:05:45 +0100263 def trace_response(self, cmd, sw, resp):
264 self.cmd2.poutput("-> %s %s" % (cmd[:10], cmd[10:]))
265 self.cmd2.poutput("<- %s: %s" % (sw, resp))
Harald Welteb2edd142021-01-08 23:29:35 +0100266
Harald Weltec91085e2022-02-10 18:05:45 +0100267 def update_prompt(self):
Harald Weltea6c0f882022-07-17 14:23:17 +0200268 if self.lchan:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200269 path_str = self.lchan.selected_file.fully_qualified_path_str(not self.numeric_path)
270 self.prompt = 'pySIM-shell (%s)> ' % (path_str)
Harald Weltec91085e2022-02-10 18:05:45 +0100271 else:
Philipp Maier7226c092022-06-01 17:58:38 +0200272 if self.card:
273 self.prompt = 'pySIM-shell (no card profile)> '
274 else:
275 self.prompt = 'pySIM-shell (no card)> '
Harald Welteb2edd142021-01-08 23:29:35 +0100276
Harald Weltec91085e2022-02-10 18:05:45 +0100277 @cmd2.with_category(CUSTOM_CATEGORY)
278 def do_intro(self, _):
279 """Display the intro banner"""
280 self.poutput(self.intro)
Philipp Maier9764de22021-11-03 10:44:39 +0100281
Harald Weltec91085e2022-02-10 18:05:45 +0100282 def do_eof(self, _: argparse.Namespace) -> bool:
283 self.poutput("")
284 return self.do_quit('')
Philipp Maier5d698e52021-09-16 13:18:01 +0200285
Harald Weltec91085e2022-02-10 18:05:45 +0100286 @cmd2.with_category(CUSTOM_CATEGORY)
287 def do_equip(self, opts):
288 """Equip pySim-shell with card"""
Harald Welte659781c2023-06-06 17:00:51 +0200289 if self.rs.profile:
290 for cmd_set in self.rs.profile.shell_cmdsets:
291 self.unregister_command_set(cmd_set)
Harald Weltec91085e2022-02-10 18:05:45 +0100292 rs, card = init_card(sl)
293 self.equip(card, rs)
Philipp Maier76667642021-09-22 16:53:22 +0200294
Philipp Maier7226c092022-06-01 17:58:38 +0200295 apdu_cmd_parser = argparse.ArgumentParser()
296 apdu_cmd_parser.add_argument('APDU', type=str, help='APDU as hex string')
Philipp Maiere7d1b672022-06-01 18:05:34 +0200297 apdu_cmd_parser.add_argument('--expect-sw', help='expect a specified status word', type=str, default=None)
Philipp Maier7226c092022-06-01 17:58:38 +0200298
299 @cmd2.with_argparser(apdu_cmd_parser)
300 def do_apdu(self, opts):
301 """Send a raw APDU to the card, and print SW + Response.
302 DANGEROUS: pySim-shell will not know any card state changes, and
303 not continue to work as expected if you e.g. select a different
304 file."""
305 data, sw = self.card._scc._tp.send_apdu(opts.APDU)
306 if data:
307 self.poutput("SW: %s, RESP: %s" % (sw, data))
308 else:
309 self.poutput("SW: %s" % sw)
Philipp Maiere7d1b672022-06-01 18:05:34 +0200310 if opts.expect_sw:
311 if not sw_match(sw, opts.expect_sw):
312 raise SwMatchError(sw, opts.expect_sw)
Philipp Maier7226c092022-06-01 17:58:38 +0200313
Harald Weltec91085e2022-02-10 18:05:45 +0100314 class InterceptStderr(list):
315 def __init__(self):
316 self._stderr_backup = sys.stderr
Philipp Maier76667642021-09-22 16:53:22 +0200317
Harald Weltec91085e2022-02-10 18:05:45 +0100318 def __enter__(self):
319 self._stringio_stderr = StringIO()
320 sys.stderr = self._stringio_stderr
321 return self
Philipp Maier76667642021-09-22 16:53:22 +0200322
Harald Weltec91085e2022-02-10 18:05:45 +0100323 def __exit__(self, *args):
324 self.stderr = self._stringio_stderr.getvalue().strip()
325 del self._stringio_stderr
326 sys.stderr = self._stderr_backup
Philipp Maier76667642021-09-22 16:53:22 +0200327
Harald Weltec91085e2022-02-10 18:05:45 +0100328 def _show_failure_sign(self):
Harald Welte961b8032023-04-27 17:30:22 +0200329 self.poutput(style(" +-------------+", fg=LIGHT_RED))
330 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
331 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
332 self.poutput(style(" + ### +", fg=LIGHT_RED))
333 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
334 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
335 self.poutput(style(" +-------------+", fg=LIGHT_RED))
Harald Weltec91085e2022-02-10 18:05:45 +0100336 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200337
Harald Weltec91085e2022-02-10 18:05:45 +0100338 def _show_success_sign(self):
Harald Welte961b8032023-04-27 17:30:22 +0200339 self.poutput(style(" +-------------+", fg=LIGHT_GREEN))
340 self.poutput(style(" + ## +", fg=LIGHT_GREEN))
341 self.poutput(style(" + ## +", fg=LIGHT_GREEN))
342 self.poutput(style(" + # ## +", fg=LIGHT_GREEN))
343 self.poutput(style(" + ## # +", fg=LIGHT_GREEN))
344 self.poutput(style(" + ## +", fg=LIGHT_GREEN))
345 self.poutput(style(" +-------------+", fg=LIGHT_GREEN))
Harald Weltec91085e2022-02-10 18:05:45 +0100346 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200347
Harald Weltec91085e2022-02-10 18:05:45 +0100348 def _process_card(self, first, script_path):
Philipp Maier76667642021-09-22 16:53:22 +0200349
Harald Weltec91085e2022-02-10 18:05:45 +0100350 # Early phase of card initialzation (this part may fail with an exception)
351 try:
352 rs, card = init_card(self.sl)
353 rc = self.equip(card, rs)
354 except:
355 self.poutput("")
356 self.poutput("Card initialization failed with an exception:")
357 self.poutput("---------------------8<---------------------")
358 traceback.print_exc()
359 self.poutput("---------------------8<---------------------")
360 self.poutput("")
361 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200362
Harald Weltec91085e2022-02-10 18:05:45 +0100363 # Actual card processing step. This part should never fail with an exception since the cmd2
364 # do_run_script method will catch any exception that might occur during script execution.
365 if rc:
366 self.poutput("")
367 self.poutput("Transcript stdout:")
368 self.poutput("---------------------8<---------------------")
369 with self.InterceptStderr() as logged:
370 self.do_run_script(script_path)
371 self.poutput("---------------------8<---------------------")
Philipp Maier76667642021-09-22 16:53:22 +0200372
Harald Weltec91085e2022-02-10 18:05:45 +0100373 self.poutput("")
374 self.poutput("Transcript stderr:")
375 if logged.stderr:
376 self.poutput("---------------------8<---------------------")
377 self.poutput(logged.stderr)
378 self.poutput("---------------------8<---------------------")
379 else:
380 self.poutput("(none)")
Philipp Maier76667642021-09-22 16:53:22 +0200381
Harald Weltec91085e2022-02-10 18:05:45 +0100382 # Check for exceptions
383 self.poutput("")
384 if "EXCEPTION of type" not in logged.stderr:
385 return 0
Philipp Maier76667642021-09-22 16:53:22 +0200386
Harald Weltec91085e2022-02-10 18:05:45 +0100387 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200388
Harald Weltec91085e2022-02-10 18:05:45 +0100389 bulk_script_parser = argparse.ArgumentParser()
390 bulk_script_parser.add_argument(
391 'script_path', help="path to the script file")
392 bulk_script_parser.add_argument('--halt_on_error', help='stop card handling if an exeption occurs',
393 action='store_true')
394 bulk_script_parser.add_argument('--tries', type=int, default=2,
395 help='how many tries before trying the next card')
396 bulk_script_parser.add_argument('--on_stop_action', type=str, default=None,
397 help='commandline to execute when card handling has stopped')
398 bulk_script_parser.add_argument('--pre_card_action', type=str, default=None,
399 help='commandline to execute before actually talking to the card')
Philipp Maier76667642021-09-22 16:53:22 +0200400
Harald Weltec91085e2022-02-10 18:05:45 +0100401 @cmd2.with_argparser(bulk_script_parser)
402 @cmd2.with_category(CUSTOM_CATEGORY)
403 def do_bulk_script(self, opts):
404 """Run script on multiple cards (bulk provisioning)"""
Philipp Maier76667642021-09-22 16:53:22 +0200405
Harald Weltec91085e2022-02-10 18:05:45 +0100406 # Make sure that the script file exists and that it is readable.
407 if not os.access(opts.script_path, os.R_OK):
408 self.poutput("Invalid script file!")
409 return
Philipp Maier76667642021-09-22 16:53:22 +0200410
Harald Weltec91085e2022-02-10 18:05:45 +0100411 success_count = 0
412 fail_count = 0
Philipp Maier76667642021-09-22 16:53:22 +0200413
Harald Weltec91085e2022-02-10 18:05:45 +0100414 first = True
415 while 1:
416 # TODO: Count consecutive failures, if more than N consecutive failures occur, then stop.
417 # The ratinale is: There may be a problem with the device, we do want to prevent that
418 # all remaining cards are fired to the error bin. This is only relevant for situations
419 # with large stacks, probably we do not need this feature right now.
Philipp Maier76667642021-09-22 16:53:22 +0200420
Harald Weltec91085e2022-02-10 18:05:45 +0100421 try:
422 # In case of failure, try multiple times.
423 for i in range(opts.tries):
424 # fetch card into reader bay
425 ch.get(first)
Philipp Maier76667642021-09-22 16:53:22 +0200426
Harald Weltec91085e2022-02-10 18:05:45 +0100427 # if necessary execute an action before we start processing the card
428 if(opts.pre_card_action):
429 os.system(opts.pre_card_action)
Philipp Maier76667642021-09-22 16:53:22 +0200430
Harald Weltec91085e2022-02-10 18:05:45 +0100431 # process the card
432 rc = self._process_card(first, opts.script_path)
433 if rc == 0:
434 success_count = success_count + 1
435 self._show_success_sign()
436 self.poutput("Statistics: success :%i, failure: %i" % (
437 success_count, fail_count))
438 break
439 else:
440 fail_count = fail_count + 1
441 self._show_failure_sign()
442 self.poutput("Statistics: success :%i, failure: %i" % (
443 success_count, fail_count))
Philipp Maier76667642021-09-22 16:53:22 +0200444
Harald Weltec91085e2022-02-10 18:05:45 +0100445 # Depending on success or failure, the card goes either in the "error" bin or in the
446 # "done" bin.
447 if rc < 0:
448 ch.error()
449 else:
450 ch.done()
Philipp Maier76667642021-09-22 16:53:22 +0200451
Harald Weltec91085e2022-02-10 18:05:45 +0100452 # In most cases it is possible to proceed with the next card, but the
453 # user may decide to halt immediately when an error occurs
454 if opts.halt_on_error and rc < 0:
455 return
Philipp Maier76667642021-09-22 16:53:22 +0200456
Harald Weltec91085e2022-02-10 18:05:45 +0100457 except (KeyboardInterrupt):
458 self.poutput("")
459 self.poutput("Terminated by user!")
460 return
461 except (SystemExit):
462 # When all cards are processed the card handler device will throw a SystemExit
463 # exception. Also Errors that are not recoverable (cards stuck etc.) will end up here.
464 # The user has the option to execute some action to make aware that the card handler
465 # needs service.
466 if(opts.on_stop_action):
467 os.system(opts.on_stop_action)
468 return
469 except:
470 self.poutput("")
471 self.poutput("Card handling failed with an exception:")
472 self.poutput("---------------------8<---------------------")
473 traceback.print_exc()
474 self.poutput("---------------------8<---------------------")
475 self.poutput("")
476 fail_count = fail_count + 1
477 self._show_failure_sign()
478 self.poutput("Statistics: success :%i, failure: %i" %
479 (success_count, fail_count))
Philipp Maierb52feed2021-09-22 16:43:13 +0200480
Harald Weltec91085e2022-02-10 18:05:45 +0100481 first = False
482
483 echo_parser = argparse.ArgumentParser()
484 echo_parser.add_argument('string', help="string to echo on the shell")
485
486 @cmd2.with_argparser(echo_parser)
487 @cmd2.with_category(CUSTOM_CATEGORY)
488 def do_echo(self, opts):
489 """Echo (print) a string on the console"""
490 self.poutput(opts.string)
491
Harald Weltefc315482022-07-23 12:49:14 +0200492 @cmd2.with_category(CUSTOM_CATEGORY)
493 def do_version(self, opts):
494 """Print the pySim software version."""
495 import pkg_resources
496 self.poutput(pkg_resources.get_distribution('pySim'))
Harald Welteb2edd142021-01-08 23:29:35 +0100497
Harald Welte31d2cf02021-04-03 10:47:29 +0200498@with_default_category('pySim Commands')
499class PySimCommands(CommandSet):
Harald Weltec91085e2022-02-10 18:05:45 +0100500 def __init__(self):
501 super().__init__()
Harald Welteb2edd142021-01-08 23:29:35 +0100502
Harald Weltec91085e2022-02-10 18:05:45 +0100503 dir_parser = argparse.ArgumentParser()
504 dir_parser.add_argument(
505 '--fids', help='Show file identifiers', action='store_true')
506 dir_parser.add_argument(
507 '--names', help='Show file names', action='store_true')
508 dir_parser.add_argument(
509 '--apps', help='Show applications', action='store_true')
510 dir_parser.add_argument(
511 '--all', help='Show all selectable identifiers and names', action='store_true')
Philipp Maier5d3e2592021-02-22 17:22:16 +0100512
Harald Weltec91085e2022-02-10 18:05:45 +0100513 @cmd2.with_argparser(dir_parser)
514 def do_dir(self, opts):
515 """Show a listing of files available in currently selected DF or MF"""
516 if opts.all:
517 flags = []
518 elif opts.fids or opts.names or opts.apps:
519 flags = ['PARENT', 'SELF']
520 if opts.fids:
521 flags += ['FIDS', 'AIDS']
522 if opts.names:
523 flags += ['FNAMES', 'ANAMES']
524 if opts.apps:
525 flags += ['ANAMES', 'AIDS']
526 else:
527 flags = ['PARENT', 'SELF', 'FNAMES', 'ANAMES']
528 selectables = list(
Harald Weltea6c0f882022-07-17 14:23:17 +0200529 self._cmd.lchan.selected_file.get_selectable_names(flags=flags))
Harald Weltec91085e2022-02-10 18:05:45 +0100530 directory_str = tabulate_str_list(
531 selectables, width=79, hspace=2, lspace=1, align_left=True)
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200532 path = self._cmd.lchan.selected_file.fully_qualified_path_str(True)
533 self._cmd.poutput(path)
534 path = self._cmd.lchan.selected_file.fully_qualified_path_str(False)
535 self._cmd.poutput(path)
Harald Weltec91085e2022-02-10 18:05:45 +0100536 self._cmd.poutput(directory_str)
537 self._cmd.poutput("%d files" % len(selectables))
Harald Welteb2edd142021-01-08 23:29:35 +0100538
Philipp Maier7b138b02022-05-31 13:42:56 +0200539 def walk(self, indent=0, action_ef=None, action_df=None, context=None, **kwargs):
Harald Weltec91085e2022-02-10 18:05:45 +0100540 """Recursively walk through the file system, starting at the currently selected DF"""
Philipp Maier7b138b02022-05-31 13:42:56 +0200541
Harald Weltea6c0f882022-07-17 14:23:17 +0200542 if isinstance(self._cmd.lchan.selected_file, CardDF):
Philipp Maier7b138b02022-05-31 13:42:56 +0200543 if action_df:
544 action_df(context, opts)
545
Harald Weltea6c0f882022-07-17 14:23:17 +0200546 files = self._cmd.lchan.selected_file.get_selectables(
Harald Weltec91085e2022-02-10 18:05:45 +0100547 flags=['FNAMES', 'ANAMES'])
548 for f in files:
Philipp Maier7b138b02022-05-31 13:42:56 +0200549 # special case: When no action is performed, just output a directory
550 if not action_ef and not action_df:
Harald Weltec91085e2022-02-10 18:05:45 +0100551 output_str = " " * indent + str(f) + (" " * 250)
552 output_str = output_str[0:25]
553 if isinstance(files[f], CardADF):
554 output_str += " " + str(files[f].aid)
555 else:
556 output_str += " " + str(files[f].fid)
557 output_str += " " + str(files[f].desc)
558 self._cmd.poutput(output_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200559
Harald Weltec91085e2022-02-10 18:05:45 +0100560 if isinstance(files[f], CardDF):
561 skip_df = False
562 try:
Harald Weltea6c0f882022-07-17 14:23:17 +0200563 fcp_dec = self._cmd.lchan.select(f, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100564 except Exception as e:
565 skip_df = True
Harald Weltea6c0f882022-07-17 14:23:17 +0200566 df = self._cmd.lchan.selected_file
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200567 df_path = df.fully_qualified_path_str(True)
568 df_skip_reason_str = df_path + \
Harald Weltec91085e2022-02-10 18:05:45 +0100569 "/" + str(f) + ", " + str(e)
570 if context:
571 context['DF_SKIP'] += 1
572 context['DF_SKIP_REASON'].append(df_skip_reason_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200573
Harald Weltec91085e2022-02-10 18:05:45 +0100574 # If the DF was skipped, we never have entered the directory
575 # below, so we must not move up.
576 if skip_df == False:
Philipp Maier7b138b02022-05-31 13:42:56 +0200577 self.walk(indent + 1, action_ef, action_df, context, **kwargs)
Harald Weltea6c0f882022-07-17 14:23:17 +0200578 fcp_dec = self._cmd.lchan.select("..", self._cmd)
Philipp Maierf408a402021-04-09 21:16:12 +0200579
Philipp Maier7b138b02022-05-31 13:42:56 +0200580 elif action_ef:
Harald Weltea6c0f882022-07-17 14:23:17 +0200581 df_before_action = self._cmd.lchan.selected_file
Philipp Maier7b138b02022-05-31 13:42:56 +0200582 action_ef(f, context, **kwargs)
Harald Weltec91085e2022-02-10 18:05:45 +0100583 # When walking through the file system tree the action must not
584 # always restore the currently selected file to the file that
585 # was selected before executing the action() callback.
Harald Weltea6c0f882022-07-17 14:23:17 +0200586 if df_before_action != self._cmd.lchan.selected_file:
Harald Weltec91085e2022-02-10 18:05:45 +0100587 raise RuntimeError("inconsistent walk, %s is currently selected but expecting %s to be selected"
Harald Weltea6c0f882022-07-17 14:23:17 +0200588 % (str(self._cmd.lchan.selected_file), str(df_before_action)))
Philipp Maierff9dae22021-02-25 17:03:21 +0100589
Harald Weltec91085e2022-02-10 18:05:45 +0100590 def do_tree(self, opts):
591 """Display a filesystem-tree with all selectable files"""
592 self.walk()
Philipp Maierff9dae22021-02-25 17:03:21 +0100593
Philipp Maier7b138b02022-05-31 13:42:56 +0200594 def export_ef(self, filename, context, as_json):
595 """ Select and export a single elementary file (EF) """
Harald Weltec91085e2022-02-10 18:05:45 +0100596 context['COUNT'] += 1
Harald Weltea6c0f882022-07-17 14:23:17 +0200597 df = self._cmd.lchan.selected_file
Philipp Maierac34dcc2021-04-01 17:19:05 +0200598
Philipp Maierea81f752022-05-19 10:13:30 +0200599 # The currently selected file (not the file we are going to export)
600 # must always be an ADF or DF. From this starting point we select
601 # the EF we want to export. To maintain consistency we will then
602 # select the current DF again (see comment below).
Harald Weltec91085e2022-02-10 18:05:45 +0100603 if not isinstance(df, CardDF):
604 raise RuntimeError(
605 "currently selected file %s is not a DF or ADF" % str(df))
Philipp Maierac34dcc2021-04-01 17:19:05 +0200606
Harald Weltec91085e2022-02-10 18:05:45 +0100607 df_path_list = df.fully_qualified_path(True)
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200608 df_path = df.fully_qualified_path_str(True)
609 df_path_fid = df.fully_qualified_path_str(False)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100610
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200611 file_str = df_path + "/" + str(filename)
Harald Weltec91085e2022-02-10 18:05:45 +0100612 self._cmd.poutput(boxed_heading_str(file_str))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100613
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200614 self._cmd.poutput("# directory: %s (%s)" % (df_path, df_path_fid))
Harald Weltec91085e2022-02-10 18:05:45 +0100615 try:
Harald Weltea6c0f882022-07-17 14:23:17 +0200616 fcp_dec = self._cmd.lchan.select(filename, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100617 self._cmd.poutput("# file: %s (%s)" % (
Harald Weltea6c0f882022-07-17 14:23:17 +0200618 self._cmd.lchan.selected_file.name, self._cmd.lchan.selected_file.fid))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100619
Harald Weltea6c0f882022-07-17 14:23:17 +0200620 structure = self._cmd.lchan.selected_file_structure()
Harald Weltec91085e2022-02-10 18:05:45 +0100621 self._cmd.poutput("# structure: %s" % str(structure))
Harald Weltea6c0f882022-07-17 14:23:17 +0200622 self._cmd.poutput("# RAW FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp_hex))
623 self._cmd.poutput("# Decoded FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100624
Harald Weltec91085e2022-02-10 18:05:45 +0100625 for f in df_path_list:
626 self._cmd.poutput("select " + str(f))
Harald Weltea6c0f882022-07-17 14:23:17 +0200627 self._cmd.poutput("select " + self._cmd.lchan.selected_file.name)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100628
Harald Weltec91085e2022-02-10 18:05:45 +0100629 if structure == 'transparent':
Harald Welte08b11ab2022-02-10 18:56:41 +0100630 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200631 result = self._cmd.lchan.read_binary_dec()
Harald Welte08b11ab2022-02-10 18:56:41 +0100632 self._cmd.poutput("update_binary_decoded '%s'" % json.dumps(result[0], cls=JsonEncoder))
633 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200634 result = self._cmd.lchan.read_binary()
Harald Welte08b11ab2022-02-10 18:56:41 +0100635 self._cmd.poutput("update_binary " + str(result[0]))
Harald Weltec91085e2022-02-10 18:05:45 +0100636 elif structure == 'cyclic' or structure == 'linear_fixed':
637 # Use number of records specified in select response
Harald Weltea6c0f882022-07-17 14:23:17 +0200638 num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
Harald Welte747a9782022-02-13 17:52:28 +0100639 if num_of_rec:
Harald Weltec91085e2022-02-10 18:05:45 +0100640 for r in range(1, num_of_rec + 1):
Harald Welte08b11ab2022-02-10 18:56:41 +0100641 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200642 result = self._cmd.lchan.read_record_dec(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100643 self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
644 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200645 result = self._cmd.lchan.read_record(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100646 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
647
Harald Weltec91085e2022-02-10 18:05:45 +0100648 # When the select response does not return the number of records, read until we hit the
649 # first record that cannot be read.
650 else:
651 r = 1
652 while True:
653 try:
Harald Welte08b11ab2022-02-10 18:56:41 +0100654 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200655 result = self._cmd.lchan.read_record_dec(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100656 self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
657 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200658 result = self._cmd.lchan.read_record(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100659 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
Harald Weltec91085e2022-02-10 18:05:45 +0100660 except SwMatchError as e:
661 # We are past the last valid record - stop
662 if e.sw_actual == "9402":
663 break
664 # Some other problem occurred
665 else:
666 raise e
Harald Weltec91085e2022-02-10 18:05:45 +0100667 r = r + 1
668 elif structure == 'ber_tlv':
Harald Weltea6c0f882022-07-17 14:23:17 +0200669 tags = self._cmd.lchan.retrieve_tags()
Harald Weltec91085e2022-02-10 18:05:45 +0100670 for t in tags:
Harald Weltea6c0f882022-07-17 14:23:17 +0200671 result = self._cmd.lchan.retrieve_data(t)
Harald Weltec91085e2022-02-10 18:05:45 +0100672 (tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
673 self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val)))
674 else:
675 raise RuntimeError(
676 'Unsupported structure "%s" of file "%s"' % (structure, filename))
677 except Exception as e:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200678 bad_file_str = df_path + "/" + str(filename) + ", " + str(e)
Harald Weltec91085e2022-02-10 18:05:45 +0100679 self._cmd.poutput("# bad file: %s" % bad_file_str)
680 context['ERR'] += 1
681 context['BAD'].append(bad_file_str)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100682
Harald Weltec91085e2022-02-10 18:05:45 +0100683 # When reading the file is done, make sure the parent file is
684 # selected again. This will be the usual case, however we need
685 # to check before since we must not select the same DF twice
Harald Weltea6c0f882022-07-17 14:23:17 +0200686 if df != self._cmd.lchan.selected_file:
687 self._cmd.lchan.select(df.fid or df.aid, self._cmd)
Philipp Maierac34dcc2021-04-01 17:19:05 +0200688
Harald Weltec91085e2022-02-10 18:05:45 +0100689 self._cmd.poutput("#")
Philipp Maier24f7bd32021-02-25 17:06:18 +0100690
Harald Weltec91085e2022-02-10 18:05:45 +0100691 export_parser = argparse.ArgumentParser()
692 export_parser.add_argument(
693 '--filename', type=str, default=None, help='only export specific file')
Harald Welte08b11ab2022-02-10 18:56:41 +0100694 export_parser.add_argument(
695 '--json', action='store_true', help='export as JSON (less reliable)')
Philipp Maier24f7bd32021-02-25 17:06:18 +0100696
Harald Weltec91085e2022-02-10 18:05:45 +0100697 @cmd2.with_argparser(export_parser)
698 def do_export(self, opts):
699 """Export files to script that can be imported back later"""
700 context = {'ERR': 0, 'COUNT': 0, 'BAD': [],
701 'DF_SKIP': 0, 'DF_SKIP_REASON': []}
Philipp Maier9a4091d2022-05-19 10:20:30 +0200702 kwargs_export = {'as_json': opts.json}
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200703 exception_str_add = ""
704
Harald Weltec91085e2022-02-10 18:05:45 +0100705 if opts.filename:
Philipp Maier7b138b02022-05-31 13:42:56 +0200706 self.export_ef(opts.filename, context, **kwargs_export)
Harald Weltec91085e2022-02-10 18:05:45 +0100707 else:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200708 try:
709 self.walk(0, self.export_ef, None, context, **kwargs_export)
710 except Exception as e:
711 print("# Stopping early here due to exception: " + str(e))
712 print("#")
713 exception_str_add = ", also had to stop early due to exception:" + str(e)
Philipp Maier80ce71f2021-04-19 21:24:23 +0200714
Harald Weltec91085e2022-02-10 18:05:45 +0100715 self._cmd.poutput(boxed_heading_str("Export summary"))
Philipp Maier80ce71f2021-04-19 21:24:23 +0200716
Harald Weltec91085e2022-02-10 18:05:45 +0100717 self._cmd.poutput("# total files visited: %u" % context['COUNT'])
718 self._cmd.poutput("# bad files: %u" % context['ERR'])
719 for b in context['BAD']:
720 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200721
Harald Weltec91085e2022-02-10 18:05:45 +0100722 self._cmd.poutput("# skipped dedicated files(s): %u" %
723 context['DF_SKIP'])
724 for b in context['DF_SKIP_REASON']:
725 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200726
Harald Weltec91085e2022-02-10 18:05:45 +0100727 if context['ERR'] and context['DF_SKIP']:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200728 raise RuntimeError("unable to export %i elementary file(s) and %i dedicated file(s)%s" % (
729 context['ERR'], context['DF_SKIP'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100730 elif context['ERR']:
731 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200732 "unable to export %i elementary file(s)%s" % (context['ERR'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100733 elif context['DF_SKIP']:
734 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200735 "unable to export %i dedicated files(s)%s" % (context['ERR'], exception_str_add))
Harald Welteb2edd142021-01-08 23:29:35 +0100736
Harald Weltec91085e2022-02-10 18:05:45 +0100737 def do_reset(self, opts):
738 """Reset the Card."""
Philipp Maiere345e112023-06-09 15:19:56 +0200739 atr = self._cmd.card.reset()
740 self._cmd.poutput('Card ATR: %s' % i2h(atr))
Harald Weltec91085e2022-02-10 18:05:45 +0100741 self._cmd.update_prompt()
Harald Weltedaf2b392021-05-03 23:17:29 +0200742
Harald Weltec91085e2022-02-10 18:05:45 +0100743 def do_desc(self, opts):
744 """Display human readable file description for the currently selected file"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200745 desc = self._cmd.lchan.selected_file.desc
Harald Weltec91085e2022-02-10 18:05:45 +0100746 if desc:
747 self._cmd.poutput(desc)
748 else:
749 self._cmd.poutput("no description available")
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200750
Harald Weltec91085e2022-02-10 18:05:45 +0100751 def do_verify_adm(self, arg):
752 """VERIFY the ADM1 PIN"""
753 if arg:
754 # use specified ADM-PIN
755 pin_adm = sanitize_pin_adm(arg)
756 else:
757 # try to find an ADM-PIN if none is specified
758 result = card_key_provider_get_field(
759 'ADM1', key='ICCID', value=self._cmd.iccid)
760 pin_adm = sanitize_pin_adm(result)
761 if pin_adm:
762 self._cmd.poutput(
763 "found ADM-PIN '%s' for ICCID '%s'" % (result, self._cmd.iccid))
764 else:
765 raise ValueError(
766 "cannot find ADM-PIN for ICCID '%s'" % (self._cmd.iccid))
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200767
Harald Weltec91085e2022-02-10 18:05:45 +0100768 if pin_adm:
769 self._cmd.card.verify_adm(h2b(pin_adm))
770 else:
771 raise ValueError("error: cannot authenticate, no adm-pin!")
772
Philipp Maier7b9e2442023-03-22 15:19:54 +0100773 def do_cardinfo(self, opts):
774 """Display information about the currently inserted card"""
775 self._cmd.poutput("Card info:")
776 self._cmd.poutput(" Name: %s" % self._cmd.card.name)
777 self._cmd.poutput(" ATR: %s" % b2h(self._cmd.card._scc.get_atr()))
778 self._cmd.poutput(" ICCID: %s" % self._cmd.iccid)
779 self._cmd.poutput(" Class-Byte: %s" % self._cmd.card._scc.cla_byte)
780 self._cmd.poutput(" Select-Ctrl: %s" % self._cmd.card._scc.sel_ctrl)
781 self._cmd.poutput(" AIDs:")
782 for a in self._cmd.rs.mf.applications:
783 self._cmd.poutput(" %s" % a)
Harald Welteb2edd142021-01-08 23:29:35 +0100784
Harald Welte31d2cf02021-04-03 10:47:29 +0200785@with_default_category('ISO7816 Commands')
786class Iso7816Commands(CommandSet):
Harald Weltec91085e2022-02-10 18:05:45 +0100787 def __init__(self):
788 super().__init__()
Harald Welte31d2cf02021-04-03 10:47:29 +0200789
Harald Weltec91085e2022-02-10 18:05:45 +0100790 def do_select(self, opts):
791 """SELECT a File (ADF/DF/EF)"""
792 if len(opts.arg_list) == 0:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200793 path = self._cmd.lchan.selected_file.fully_qualified_path_str(True)
794 path_fid = self._cmd.lchan.selected_file.fully_qualified_path_str(False)
795 self._cmd.poutput("currently selected file: %s (%s)" % (path, path_fid))
Harald Weltec91085e2022-02-10 18:05:45 +0100796 return
Harald Welte31d2cf02021-04-03 10:47:29 +0200797
Harald Weltec91085e2022-02-10 18:05:45 +0100798 path = opts.arg_list[0]
Harald Weltea6c0f882022-07-17 14:23:17 +0200799 fcp_dec = self._cmd.lchan.select(path, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100800 self._cmd.update_prompt()
801 self._cmd.poutput_json(fcp_dec)
Harald Welte31d2cf02021-04-03 10:47:29 +0200802
Harald Weltec91085e2022-02-10 18:05:45 +0100803 def complete_select(self, text, line, begidx, endidx) -> List[str]:
804 """Command Line tab completion for SELECT"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200805 index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
Harald Weltec91085e2022-02-10 18:05:45 +0100806 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200807
Harald Weltec91085e2022-02-10 18:05:45 +0100808 def get_code(self, code):
809 """Use code either directly or try to get it from external data source"""
810 auto = ('PIN1', 'PIN2', 'PUK1', 'PUK2')
Harald Welte31d2cf02021-04-03 10:47:29 +0200811
Harald Weltec91085e2022-02-10 18:05:45 +0100812 if str(code).upper() not in auto:
813 return sanitize_pin_adm(code)
Harald Welte31d2cf02021-04-03 10:47:29 +0200814
Harald Weltec91085e2022-02-10 18:05:45 +0100815 result = card_key_provider_get_field(
816 str(code), key='ICCID', value=self._cmd.iccid)
817 result = sanitize_pin_adm(result)
818 if result:
819 self._cmd.poutput("found %s '%s' for ICCID '%s'" %
820 (code.upper(), result, self._cmd.iccid))
821 else:
822 self._cmd.poutput("cannot find %s for ICCID '%s'" %
823 (code.upper(), self._cmd.iccid))
824 return result
Harald Welte31d2cf02021-04-03 10:47:29 +0200825
Harald Weltec91085e2022-02-10 18:05:45 +0100826 verify_chv_parser = argparse.ArgumentParser()
827 verify_chv_parser.add_argument(
828 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
829 verify_chv_parser.add_argument(
830 '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 +0200831
Harald Weltec91085e2022-02-10 18:05:45 +0100832 @cmd2.with_argparser(verify_chv_parser)
833 def do_verify_chv(self, opts):
Harald Welte12af7932022-02-15 16:39:08 +0100834 """Verify (authenticate) using specified CHV (PIN) code, which is how the specifications
835 call it if you authenticate yourself using the specified PIN. There usually is at least PIN1 and
836 PIN2."""
Harald Weltec91085e2022-02-10 18:05:45 +0100837 pin = self.get_code(opts.pin_code)
838 (data, sw) = self._cmd.card._scc.verify_chv(opts.pin_nr, h2b(pin))
839 self._cmd.poutput("CHV verification successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200840
Harald Weltec91085e2022-02-10 18:05:45 +0100841 unblock_chv_parser = argparse.ArgumentParser()
842 unblock_chv_parser.add_argument(
843 '--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
844 unblock_chv_parser.add_argument(
845 'puk_code', type=str, help='PUK code digits \"PUK1\" or \"PUK2\" to get PUK code from external data source')
846 unblock_chv_parser.add_argument(
847 '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 +0200848
Harald Weltec91085e2022-02-10 18:05:45 +0100849 @cmd2.with_argparser(unblock_chv_parser)
850 def do_unblock_chv(self, opts):
851 """Unblock PIN code using specified PUK code"""
852 new_pin = self.get_code(opts.new_pin_code)
853 puk = self.get_code(opts.puk_code)
854 (data, sw) = self._cmd.card._scc.unblock_chv(
855 opts.pin_nr, h2b(puk), h2b(new_pin))
856 self._cmd.poutput("CHV unblock successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200857
Harald Weltec91085e2022-02-10 18:05:45 +0100858 change_chv_parser = argparse.ArgumentParser()
859 change_chv_parser.add_argument(
860 '--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
861 change_chv_parser.add_argument(
862 'pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
863 change_chv_parser.add_argument(
864 '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 +0200865
Harald Weltec91085e2022-02-10 18:05:45 +0100866 @cmd2.with_argparser(change_chv_parser)
867 def do_change_chv(self, opts):
868 """Change PIN code to a new PIN code"""
869 new_pin = self.get_code(opts.new_pin_code)
870 pin = self.get_code(opts.pin_code)
871 (data, sw) = self._cmd.card._scc.change_chv(
872 opts.pin_nr, h2b(pin), h2b(new_pin))
873 self._cmd.poutput("CHV change successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200874
Harald Weltec91085e2022-02-10 18:05:45 +0100875 disable_chv_parser = argparse.ArgumentParser()
876 disable_chv_parser.add_argument(
877 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
878 disable_chv_parser.add_argument(
879 '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 +0200880
Harald Weltec91085e2022-02-10 18:05:45 +0100881 @cmd2.with_argparser(disable_chv_parser)
882 def do_disable_chv(self, opts):
883 """Disable PIN code using specified PIN code"""
884 pin = self.get_code(opts.pin_code)
885 (data, sw) = self._cmd.card._scc.disable_chv(opts.pin_nr, h2b(pin))
886 self._cmd.poutput("CHV disable successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200887
Harald Weltec91085e2022-02-10 18:05:45 +0100888 enable_chv_parser = argparse.ArgumentParser()
889 enable_chv_parser.add_argument(
890 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
891 enable_chv_parser.add_argument(
892 '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 +0200893
Harald Weltec91085e2022-02-10 18:05:45 +0100894 @cmd2.with_argparser(enable_chv_parser)
895 def do_enable_chv(self, opts):
896 """Enable PIN code using specified PIN code"""
897 pin = self.get_code(opts.pin_code)
898 (data, sw) = self._cmd.card._scc.enable_chv(opts.pin_nr, h2b(pin))
899 self._cmd.poutput("CHV enable successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200900
Harald Weltec91085e2022-02-10 18:05:45 +0100901 def do_deactivate_file(self, opts):
Harald Welte799c3542022-02-15 15:56:28 +0100902 """Deactivate the currently selected EF"""
Harald Weltec91085e2022-02-10 18:05:45 +0100903 (data, sw) = self._cmd.card._scc.deactivate_file()
Harald Weltea4631612021-04-10 18:17:55 +0200904
Harald Welte799c3542022-02-15 15:56:28 +0100905 activate_file_parser = argparse.ArgumentParser()
906 activate_file_parser.add_argument('NAME', type=str, help='File name or FID of file to activate')
907 @cmd2.with_argparser(activate_file_parser)
Harald Weltec91085e2022-02-10 18:05:45 +0100908 def do_activate_file(self, opts):
Harald Welte12af7932022-02-15 16:39:08 +0100909 """Activate the specified EF. This used to be called REHABILITATE in TS 11.11 for classic
910 SIM. You need to specify the name or FID of the file to activate."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200911 (data, sw) = self._cmd.lchan.activate_file(opts.NAME)
Harald Welte485692b2021-05-25 22:21:44 +0200912
Harald Weltec91085e2022-02-10 18:05:45 +0100913 def complete_activate_file(self, text, line, begidx, endidx) -> List[str]:
914 """Command Line tab completion for ACTIVATE FILE"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200915 index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
Harald Weltec91085e2022-02-10 18:05:45 +0100916 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200917
Harald Weltec91085e2022-02-10 18:05:45 +0100918 open_chan_parser = argparse.ArgumentParser()
919 open_chan_parser.add_argument(
920 'chan_nr', type=int, default=0, help='Channel Number')
Harald Welte703f9332021-04-10 18:39:32 +0200921
Harald Weltec91085e2022-02-10 18:05:45 +0100922 @cmd2.with_argparser(open_chan_parser)
923 def do_open_channel(self, opts):
924 """Open a logical channel."""
925 (data, sw) = self._cmd.card._scc.manage_channel(
926 mode='open', lchan_nr=opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200927
Harald Weltec91085e2022-02-10 18:05:45 +0100928 close_chan_parser = argparse.ArgumentParser()
929 close_chan_parser.add_argument(
930 'chan_nr', type=int, default=0, help='Channel Number')
Harald Welte703f9332021-04-10 18:39:32 +0200931
Harald Weltec91085e2022-02-10 18:05:45 +0100932 @cmd2.with_argparser(close_chan_parser)
933 def do_close_channel(self, opts):
934 """Close a logical channel."""
935 (data, sw) = self._cmd.card._scc.manage_channel(
936 mode='close', lchan_nr=opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200937
Harald Weltec91085e2022-02-10 18:05:45 +0100938 def do_status(self, opts):
939 """Perform the STATUS command."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200940 fcp_dec = self._cmd.lchan.status()
Harald Weltec91085e2022-02-10 18:05:45 +0100941 self._cmd.poutput_json(fcp_dec)
Harald Welte34b05d32021-05-25 22:03:13 +0200942
Harald Welteec950532021-10-20 13:09:00 +0200943
Harald Weltecab26c72022-08-06 16:12:30 +0200944class Proact(ProactiveHandler):
945 def receive_fetch(self, pcmd: ProactiveCommand):
946 # print its parsed representation
947 print(pcmd.decoded)
948 # TODO: implement the basics, such as SMS Sending, ...
949
950
Harald Welte703f9332021-04-10 18:39:32 +0200951
Harald Weltef422eb12023-06-09 11:15:09 +0200952option_parser = argparse.ArgumentParser(description='interactive SIM card shell',
Harald Weltef2e761c2021-04-11 11:56:44 +0200953 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
Harald Welte28c24312021-04-11 12:19:36 +0200954argparse_add_reader_args(option_parser)
Harald Weltec8ff0262021-04-11 12:06:13 +0200955
956global_group = option_parser.add_argument_group('General Options')
957global_group.add_argument('--script', metavar='PATH', default=None,
Harald Welte28c24312021-04-11 12:19:36 +0200958 help='script with pySim-shell commands to be executed automatically at start-up')
Harald Weltec91085e2022-02-10 18:05:45 +0100959global_group.add_argument('--csv', metavar='FILE',
960 default=None, help='Read card data from CSV file')
Philipp Maier76667642021-09-22 16:53:22 +0200961global_group.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
Harald Weltec91085e2022-02-10 18:05:45 +0100962 help="Use automatic card handling machine")
Harald Weltec8ff0262021-04-11 12:06:13 +0200963
964adm_group = global_group.add_mutually_exclusive_group()
965adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
966 help='ADM PIN used for provisioning (overwrites default)')
967adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM1_HEX', dest='pin_adm_hex', default=None,
968 help='ADM PIN used for provisioning, as hex string (16 characters long)')
Harald Welteb2edd142021-01-08 23:29:35 +0100969
970
971if __name__ == '__main__':
972
Harald Weltec91085e2022-02-10 18:05:45 +0100973 # Parse options
974 opts = option_parser.parse_args()
Harald Welteb2edd142021-01-08 23:29:35 +0100975
Harald Weltec91085e2022-02-10 18:05:45 +0100976 # If a script file is specified, be sure that it actually exists
977 if opts.script:
978 if not os.access(opts.script, os.R_OK):
979 print("Invalid script file!")
980 sys.exit(2)
Philipp Maier13e258d2021-04-08 17:48:49 +0200981
Harald Weltec91085e2022-02-10 18:05:45 +0100982 # Register csv-file as card data provider, either from specified CSV
983 # or from CSV file in home directory
984 csv_default = str(Path.home()) + "/.osmocom/pysim/card_data.csv"
985 if opts.csv:
986 card_key_provider_register(CardKeyProviderCsv(opts.csv))
987 if os.path.isfile(csv_default):
988 card_key_provider_register(CardKeyProviderCsv(csv_default))
Philipp Maier2b11c322021-03-17 12:37:39 +0100989
Harald Weltec91085e2022-02-10 18:05:45 +0100990 # Init card reader driver
Harald Weltecab26c72022-08-06 16:12:30 +0200991 sl = init_reader(opts, proactive_handler = Proact())
Harald Weltec91085e2022-02-10 18:05:45 +0100992 if sl is None:
993 exit(1)
Philipp Maierea95c392021-09-16 13:10:19 +0200994
Harald Weltec91085e2022-02-10 18:05:45 +0100995 # Create command layer
996 scc = SimCardCommands(transport=sl)
Philipp Maierea95c392021-09-16 13:10:19 +0200997
Harald Weltec91085e2022-02-10 18:05:45 +0100998 # Create a card handler (for bulk provisioning)
999 if opts.card_handler_config:
1000 ch = CardHandlerAuto(None, opts.card_handler_config)
1001 else:
1002 ch = CardHandler(sl)
Philipp Maier76667642021-09-22 16:53:22 +02001003
Harald Weltec91085e2022-02-10 18:05:45 +01001004 # Detect and initialize the card in the reader. This may fail when there
1005 # is no card in the reader or the card is unresponsive. PysimApp is
1006 # able to tolerate and recover from that.
1007 try:
1008 rs, card = init_card(sl)
1009 app = PysimApp(card, rs, sl, ch, opts.script)
1010 except:
1011 print("Card initialization failed with an exception:")
1012 print("---------------------8<---------------------")
1013 traceback.print_exc()
1014 print("---------------------8<---------------------")
1015 print("(you may still try to recover from this manually by using the 'equip' command.)")
1016 print(
1017 " it should also be noted that some readers may behave strangely when no card")
1018 print(" is inserted.)")
1019 print("")
Philipp Maier7226c092022-06-01 17:58:38 +02001020 app = PysimApp(card, None, sl, ch, opts.script)
Philipp Maierea95c392021-09-16 13:10:19 +02001021
Harald Weltec91085e2022-02-10 18:05:45 +01001022 # If the user supplies an ADM PIN at via commandline args authenticate
1023 # immediately so that the user does not have to use the shell commands
1024 pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
1025 if pin_adm:
1026 if not card:
1027 print("Card error, cannot do ADM verification with supplied ADM pin now.")
1028 try:
1029 card.verify_adm(h2b(pin_adm))
1030 except Exception as e:
1031 print(e)
Philipp Maier228c98e2021-03-10 20:14:06 +01001032
Harald Weltec91085e2022-02-10 18:05:45 +01001033 app.cmdloop()