blob: 51cd9c6fabebe5e4657fa6def673a616cff95d6f [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"):
30 from cmd2 import fg, bg
31 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 Welte95ce6b12021-10-20 18:40:54 +020064from pySim.ara_m import CardApplicationARAM
Harald Welte34eb5042022-02-21 17:19:28 +010065from pySim.global_platform import CardApplicationISD
Harald Welte2a33ad22021-10-20 10:14:18 +020066from pySim.gsm_r import DF_EIRENE
Harald Weltecab26c72022-08-06 16:12:30 +020067from pySim.cat import ProactiveCommand
Harald Welteb2edd142021-01-08 23:29:35 +010068
Harald Welte4c1dca02021-10-14 17:48:25 +020069# we need to import this module so that the SysmocomSJA2 sub-class of
70# CardModel is created, which will add the ATR-based matching and
71# calling of SysmocomSJA2.add_files. See CardModel.apply_matching_models
Harald Weltef44256c2021-10-14 15:53:39 +020072import pySim.sysmocom_sja2
73
Harald Welte4442b3d2021-04-03 09:00:16 +020074from pySim.card_key_provider import CardKeyProviderCsv, card_key_provider_register, card_key_provider_get_field
Philipp Maier2b11c322021-03-17 12:37:39 +010075
Harald Weltec91085e2022-02-10 18:05:45 +010076
Philipp Maierea95c392021-09-16 13:10:19 +020077def init_card(sl):
Harald Weltec91085e2022-02-10 18:05:45 +010078 """
79 Detect card in reader and setup card profile and runtime state. This
80 function must be called at least once on startup. The card and runtime
81 state object (rs) is required for all pySim-shell commands.
82 """
Philipp Maierea95c392021-09-16 13:10:19 +020083
Harald Weltec91085e2022-02-10 18:05:45 +010084 # Wait up to three seconds for a card in reader and try to detect
85 # the card type.
86 print("Waiting for card...")
87 try:
88 sl.wait_for_card(3)
89 except NoCardError:
90 print("No card detected!")
91 return None, None
92 except:
93 print("Card not readable!")
94 return None, None
Philipp Maierea95c392021-09-16 13:10:19 +020095
Philipp Maier24031252022-06-14 16:16:42 +020096 generic_card = False
Harald Weltec91085e2022-02-10 18:05:45 +010097 card = card_detect("auto", scc)
98 if card is None:
99 print("Warning: Could not detect card type - assuming a generic card type...")
100 card = SimCard(scc)
Philipp Maier24031252022-06-14 16:16:42 +0200101 generic_card = True
Philipp Maiera028c7d2021-11-08 16:12:03 +0100102
Harald Weltec91085e2022-02-10 18:05:45 +0100103 profile = CardProfile.pick(scc)
104 if profile is None:
105 print("Unsupported card type!")
Philipp Maier7226c092022-06-01 17:58:38 +0200106 return None, card
Philipp Maierea95c392021-09-16 13:10:19 +0200107
Philipp Maier24031252022-06-14 16:16:42 +0200108 # ETSI TS 102 221, Table 9.3 specifies a default for the PIN key
109 # references, however card manufactures may still decide to pick an
110 # arbitrary key reference. In case we run on a generic card class that is
111 # detected as an UICC, we will pick the key reference that is officially
112 # specified.
113 if generic_card and isinstance(profile, CardProfileUICC):
114 card._adm_chv_num = 0x0A
115
Harald Weltec91085e2022-02-10 18:05:45 +0100116 print("Info: Card is of type: %s" % str(profile))
Philipp Maiera028c7d2021-11-08 16:12:03 +0100117
Harald Weltec91085e2022-02-10 18:05:45 +0100118 # FIXME: This shouln't be here, the profile should add the applications,
119 # however, we cannot simply put his into ts_102_221.py since we would
120 # have to e.g. import CardApplicationUSIM from ts_31_102.py, which already
121 # imports from ts_102_221.py. This means we will end up with a circular
122 # import, which needs to be resolved first.
123 if isinstance(profile, CardProfileUICC):
124 profile.add_application(CardApplicationUSIM())
125 profile.add_application(CardApplicationISIM())
126 profile.add_application(CardApplicationARAM())
Harald Welte34eb5042022-02-21 17:19:28 +0100127 profile.add_application(CardApplicationISD())
Philipp Maiera028c7d2021-11-08 16:12:03 +0100128
Harald Weltec91085e2022-02-10 18:05:45 +0100129 # Create runtime state with card profile
130 rs = RuntimeState(card, profile)
Philipp Maierea95c392021-09-16 13:10:19 +0200131
Harald Weltec91085e2022-02-10 18:05:45 +0100132 # FIXME: This is an GSM-R related file, it needs to be added throughout,
133 # the profile. At the moment we add it for all cards, this won't hurt,
134 # but regular SIM and UICC will not have it and fail to select it.
135 rs.mf.add_file(DF_EIRENE())
Philipp Maierea95c392021-09-16 13:10:19 +0200136
Harald Weltec91085e2022-02-10 18:05:45 +0100137 CardModel.apply_matching_models(scc, rs)
Harald Weltef44256c2021-10-14 15:53:39 +0200138
Harald Weltec91085e2022-02-10 18:05:45 +0100139 # inform the transport that we can do context-specific SW interpretation
140 sl.set_sw_interpreter(rs)
Philipp Maierea95c392021-09-16 13:10:19 +0200141
Harald Weltec91085e2022-02-10 18:05:45 +0100142 return rs, card
143
Philipp Maier2b11c322021-03-17 12:37:39 +0100144
Harald Welteb2edd142021-01-08 23:29:35 +0100145class PysimApp(cmd2.Cmd):
Harald Weltec91085e2022-02-10 18:05:45 +0100146 CUSTOM_CATEGORY = 'pySim Commands'
Philipp Maier76667642021-09-22 16:53:22 +0200147
Harald Weltec91085e2022-02-10 18:05:45 +0100148 def __init__(self, card, rs, sl, ch, script=None):
Harald Weltec85d4062023-04-27 17:10:17 +0200149 if version.parse(cmd2.__version__) < version.parse("2.0.0"):
150 kwargs = {'use_ipython': True}
151 else:
152 kwargs = {'include_ipy': True}
153
Harald Weltec91085e2022-02-10 18:05:45 +0100154 super().__init__(persistent_history_file='~/.pysim_shell_history', allow_cli_args=False,
Harald Weltec85d4062023-04-27 17:10:17 +0200155 auto_load_commands=False, startup_script=script, **kwargs)
Harald Welte961b8032023-04-27 17:30:22 +0200156 self.intro = style('Welcome to pySim-shell!', fg=RED)
Harald Weltec91085e2022-02-10 18:05:45 +0100157 self.default_category = 'pySim-shell built-in commands'
158 self.card = None
159 self.rs = None
Harald Weltea6c0f882022-07-17 14:23:17 +0200160 self.lchan = None
161 self.py_locals = {'card': self.card, 'rs': self.rs, 'lchan': self.lchan}
Harald Weltec91085e2022-02-10 18:05:45 +0100162 self.sl = sl
163 self.ch = ch
Harald Welte1748b932021-04-06 21:12:25 +0200164
Harald Weltec91085e2022-02-10 18:05:45 +0100165 self.numeric_path = False
Harald Weltec91085e2022-02-10 18:05:45 +0100166 self.conserve_write = True
Harald Weltec91085e2022-02-10 18:05:45 +0100167 self.json_pretty_print = True
Harald Weltec91085e2022-02-10 18:05:45 +0100168 self.apdu_trace = False
Philipp Maier5d698e52021-09-16 13:18:01 +0200169
Harald Welte93aac3a2023-04-27 15:18:13 +0200170 if version.parse(cmd2.__version__) < version.parse("2.0.0"):
171 self.add_settable(cmd2.Settable('numeric_path', bool, 'Print File IDs instead of names',
172 onchange_cb=self._onchange_numeric_path))
173 self.add_settable(cmd2.Settable('conserve_write', bool, 'Read and compare before write',
174 onchange_cb=self._onchange_conserve_write))
175 self.add_settable(cmd2.Settable('json_pretty_print', bool, 'Pretty-Print JSON output'))
176 self.add_settable(cmd2.Settable('apdu_trace', bool, 'Trace and display APDUs exchanged with card',
177 onchange_cb=self._onchange_apdu_trace))
178 else:
179 self.add_settable(cmd2.Settable('numeric_path', bool, 'Print File IDs instead of names', self, \
180 onchange_cb=self._onchange_numeric_path)) # pylint: disable=too-many-function-args
181 self.add_settable(cmd2.Settable('conserve_write', bool, 'Read and compare before write', self, \
182 onchange_cb=self._onchange_conserve_write)) # pylint: disable=too-many-function-args
183 self.add_settable(cmd2.Settable('json_pretty_print', bool, 'Pretty-Print JSON output', self)) # pylint: disable=too-many-function-args
184 self.add_settable(cmd2.Settable('apdu_trace', bool, 'Trace and display APDUs exchanged with card', self, \
185 onchange_cb=self._onchange_apdu_trace)) # pylint: disable=too-many-function-args
Harald Weltec91085e2022-02-10 18:05:45 +0100186 self.equip(card, rs)
Philipp Maier5d698e52021-09-16 13:18:01 +0200187
Harald Weltec91085e2022-02-10 18:05:45 +0100188 def equip(self, card, rs):
189 """
190 Equip pySim-shell with the supplied card and runtime state, add (or remove) all required settables and
191 and commands to enable card operations.
192 """
Philipp Maier76667642021-09-22 16:53:22 +0200193
Harald Weltec91085e2022-02-10 18:05:45 +0100194 rc = False
Philipp Maier5d698e52021-09-16 13:18:01 +0200195
Harald Weltec91085e2022-02-10 18:05:45 +0100196 # Unequip everything from pySim-shell that would not work in unequipped state
197 if self.rs:
Harald Weltea6c0f882022-07-17 14:23:17 +0200198 lchan = self.rs.lchan[0]
199 lchan.unregister_cmds(self)
Harald Weltec91085e2022-02-10 18:05:45 +0100200 for cmds in [Iso7816Commands, PySimCommands]:
201 cmd_set = self.find_commandsets(cmds)
202 if cmd_set:
203 self.unregister_command_set(cmd_set[0])
Philipp Maier5d698e52021-09-16 13:18:01 +0200204
Harald Weltec91085e2022-02-10 18:05:45 +0100205 self.card = card
206 self.rs = rs
Philipp Maier5d698e52021-09-16 13:18:01 +0200207
Harald Weltec91085e2022-02-10 18:05:45 +0100208 # When a card object and a runtime state is present, (re)equip pySim-shell with everything that is
209 # needed to operate on cards.
210 if self.card and self.rs:
Harald Weltea6c0f882022-07-17 14:23:17 +0200211 self.lchan = self.rs.lchan[0]
Harald Weltec91085e2022-02-10 18:05:45 +0100212 self._onchange_conserve_write(
213 'conserve_write', False, self.conserve_write)
214 self._onchange_apdu_trace('apdu_trace', False, self.apdu_trace)
215 self.register_command_set(Iso7816Commands())
Harald Welte3c9b7842021-10-19 21:44:24 +0200216 self.register_command_set(Ts102222Commands())
Harald Weltec91085e2022-02-10 18:05:45 +0100217 self.register_command_set(PySimCommands())
218 self.iccid, sw = self.card.read_iccid()
Harald Weltea6c0f882022-07-17 14:23:17 +0200219 self.lchan.select('MF', self)
Harald Weltec91085e2022-02-10 18:05:45 +0100220 rc = True
221 else:
222 self.poutput("pySim-shell not equipped!")
Philipp Maier5d698e52021-09-16 13:18:01 +0200223
Harald Weltec91085e2022-02-10 18:05:45 +0100224 self.update_prompt()
225 return rc
Harald Welteb2edd142021-01-08 23:29:35 +0100226
Harald Weltec91085e2022-02-10 18:05:45 +0100227 def poutput_json(self, data, force_no_pretty=False):
228 """like cmd2.poutput() but for a JSON serializable dict."""
229 if force_no_pretty or self.json_pretty_print == False:
230 output = json.dumps(data, cls=JsonEncoder)
231 else:
232 output = json.dumps(data, cls=JsonEncoder, indent=4)
233 self.poutput(output)
Harald Welteb2edd142021-01-08 23:29:35 +0100234
Harald Weltec91085e2022-02-10 18:05:45 +0100235 def _onchange_numeric_path(self, param_name, old, new):
236 self.update_prompt()
Philipp Maier38c74f62021-03-17 17:19:52 +0100237
Harald Weltec91085e2022-02-10 18:05:45 +0100238 def _onchange_conserve_write(self, param_name, old, new):
239 if self.rs:
240 self.rs.conserve_write = new
Harald Welte7829d8a2021-04-10 11:28:53 +0200241
Harald Weltec91085e2022-02-10 18:05:45 +0100242 def _onchange_apdu_trace(self, param_name, old, new):
243 if self.card:
244 if new == True:
245 self.card._scc._tp.apdu_tracer = self.Cmd2ApduTracer(self)
246 else:
247 self.card._scc._tp.apdu_tracer = None
Harald Welte7829d8a2021-04-10 11:28:53 +0200248
Harald Weltec91085e2022-02-10 18:05:45 +0100249 class Cmd2ApduTracer(ApduTracer):
250 def __init__(self, cmd2_app):
251 self.cmd2 = app
Harald Welte7829d8a2021-04-10 11:28:53 +0200252
Harald Weltec91085e2022-02-10 18:05:45 +0100253 def trace_response(self, cmd, sw, resp):
254 self.cmd2.poutput("-> %s %s" % (cmd[:10], cmd[10:]))
255 self.cmd2.poutput("<- %s: %s" % (sw, resp))
Harald Welteb2edd142021-01-08 23:29:35 +0100256
Harald Weltec91085e2022-02-10 18:05:45 +0100257 def update_prompt(self):
Harald Weltea6c0f882022-07-17 14:23:17 +0200258 if self.lchan:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200259 path_str = self.lchan.selected_file.fully_qualified_path_str(not self.numeric_path)
260 self.prompt = 'pySIM-shell (%s)> ' % (path_str)
Harald Weltec91085e2022-02-10 18:05:45 +0100261 else:
Philipp Maier7226c092022-06-01 17:58:38 +0200262 if self.card:
263 self.prompt = 'pySIM-shell (no card profile)> '
264 else:
265 self.prompt = 'pySIM-shell (no card)> '
Harald Welteb2edd142021-01-08 23:29:35 +0100266
Harald Weltec91085e2022-02-10 18:05:45 +0100267 @cmd2.with_category(CUSTOM_CATEGORY)
268 def do_intro(self, _):
269 """Display the intro banner"""
270 self.poutput(self.intro)
Philipp Maier9764de22021-11-03 10:44:39 +0100271
Harald Weltec91085e2022-02-10 18:05:45 +0100272 def do_eof(self, _: argparse.Namespace) -> bool:
273 self.poutput("")
274 return self.do_quit('')
Philipp Maier5d698e52021-09-16 13:18:01 +0200275
Harald Weltec91085e2022-02-10 18:05:45 +0100276 @cmd2.with_category(CUSTOM_CATEGORY)
277 def do_equip(self, opts):
278 """Equip pySim-shell with card"""
279 rs, card = init_card(sl)
280 self.equip(card, rs)
Philipp Maier76667642021-09-22 16:53:22 +0200281
Philipp Maier7226c092022-06-01 17:58:38 +0200282 apdu_cmd_parser = argparse.ArgumentParser()
283 apdu_cmd_parser.add_argument('APDU', type=str, help='APDU as hex string')
Philipp Maiere7d1b672022-06-01 18:05:34 +0200284 apdu_cmd_parser.add_argument('--expect-sw', help='expect a specified status word', type=str, default=None)
Philipp Maier7226c092022-06-01 17:58:38 +0200285
286 @cmd2.with_argparser(apdu_cmd_parser)
287 def do_apdu(self, opts):
288 """Send a raw APDU to the card, and print SW + Response.
289 DANGEROUS: pySim-shell will not know any card state changes, and
290 not continue to work as expected if you e.g. select a different
291 file."""
292 data, sw = self.card._scc._tp.send_apdu(opts.APDU)
293 if data:
294 self.poutput("SW: %s, RESP: %s" % (sw, data))
295 else:
296 self.poutput("SW: %s" % sw)
Philipp Maiere7d1b672022-06-01 18:05:34 +0200297 if opts.expect_sw:
298 if not sw_match(sw, opts.expect_sw):
299 raise SwMatchError(sw, opts.expect_sw)
Philipp Maier7226c092022-06-01 17:58:38 +0200300
Harald Weltec91085e2022-02-10 18:05:45 +0100301 class InterceptStderr(list):
302 def __init__(self):
303 self._stderr_backup = sys.stderr
Philipp Maier76667642021-09-22 16:53:22 +0200304
Harald Weltec91085e2022-02-10 18:05:45 +0100305 def __enter__(self):
306 self._stringio_stderr = StringIO()
307 sys.stderr = self._stringio_stderr
308 return self
Philipp Maier76667642021-09-22 16:53:22 +0200309
Harald Weltec91085e2022-02-10 18:05:45 +0100310 def __exit__(self, *args):
311 self.stderr = self._stringio_stderr.getvalue().strip()
312 del self._stringio_stderr
313 sys.stderr = self._stderr_backup
Philipp Maier76667642021-09-22 16:53:22 +0200314
Harald Weltec91085e2022-02-10 18:05:45 +0100315 def _show_failure_sign(self):
Harald Welte961b8032023-04-27 17:30:22 +0200316 self.poutput(style(" +-------------+", fg=LIGHT_RED))
317 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
318 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
319 self.poutput(style(" + ### +", fg=LIGHT_RED))
320 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
321 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
322 self.poutput(style(" +-------------+", fg=LIGHT_RED))
Harald Weltec91085e2022-02-10 18:05:45 +0100323 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200324
Harald Weltec91085e2022-02-10 18:05:45 +0100325 def _show_success_sign(self):
Harald Welte961b8032023-04-27 17:30:22 +0200326 self.poutput(style(" +-------------+", fg=LIGHT_GREEN))
327 self.poutput(style(" + ## +", fg=LIGHT_GREEN))
328 self.poutput(style(" + ## +", fg=LIGHT_GREEN))
329 self.poutput(style(" + # ## +", fg=LIGHT_GREEN))
330 self.poutput(style(" + ## # +", fg=LIGHT_GREEN))
331 self.poutput(style(" + ## +", fg=LIGHT_GREEN))
332 self.poutput(style(" +-------------+", fg=LIGHT_GREEN))
Harald Weltec91085e2022-02-10 18:05:45 +0100333 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200334
Harald Weltec91085e2022-02-10 18:05:45 +0100335 def _process_card(self, first, script_path):
Philipp Maier76667642021-09-22 16:53:22 +0200336
Harald Weltec91085e2022-02-10 18:05:45 +0100337 # Early phase of card initialzation (this part may fail with an exception)
338 try:
339 rs, card = init_card(self.sl)
340 rc = self.equip(card, rs)
341 except:
342 self.poutput("")
343 self.poutput("Card initialization failed with an exception:")
344 self.poutput("---------------------8<---------------------")
345 traceback.print_exc()
346 self.poutput("---------------------8<---------------------")
347 self.poutput("")
348 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200349
Harald Weltec91085e2022-02-10 18:05:45 +0100350 # Actual card processing step. This part should never fail with an exception since the cmd2
351 # do_run_script method will catch any exception that might occur during script execution.
352 if rc:
353 self.poutput("")
354 self.poutput("Transcript stdout:")
355 self.poutput("---------------------8<---------------------")
356 with self.InterceptStderr() as logged:
357 self.do_run_script(script_path)
358 self.poutput("---------------------8<---------------------")
Philipp Maier76667642021-09-22 16:53:22 +0200359
Harald Weltec91085e2022-02-10 18:05:45 +0100360 self.poutput("")
361 self.poutput("Transcript stderr:")
362 if logged.stderr:
363 self.poutput("---------------------8<---------------------")
364 self.poutput(logged.stderr)
365 self.poutput("---------------------8<---------------------")
366 else:
367 self.poutput("(none)")
Philipp Maier76667642021-09-22 16:53:22 +0200368
Harald Weltec91085e2022-02-10 18:05:45 +0100369 # Check for exceptions
370 self.poutput("")
371 if "EXCEPTION of type" not in logged.stderr:
372 return 0
Philipp Maier76667642021-09-22 16:53:22 +0200373
Harald Weltec91085e2022-02-10 18:05:45 +0100374 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200375
Harald Weltec91085e2022-02-10 18:05:45 +0100376 bulk_script_parser = argparse.ArgumentParser()
377 bulk_script_parser.add_argument(
378 'script_path', help="path to the script file")
379 bulk_script_parser.add_argument('--halt_on_error', help='stop card handling if an exeption occurs',
380 action='store_true')
381 bulk_script_parser.add_argument('--tries', type=int, default=2,
382 help='how many tries before trying the next card')
383 bulk_script_parser.add_argument('--on_stop_action', type=str, default=None,
384 help='commandline to execute when card handling has stopped')
385 bulk_script_parser.add_argument('--pre_card_action', type=str, default=None,
386 help='commandline to execute before actually talking to the card')
Philipp Maier76667642021-09-22 16:53:22 +0200387
Harald Weltec91085e2022-02-10 18:05:45 +0100388 @cmd2.with_argparser(bulk_script_parser)
389 @cmd2.with_category(CUSTOM_CATEGORY)
390 def do_bulk_script(self, opts):
391 """Run script on multiple cards (bulk provisioning)"""
Philipp Maier76667642021-09-22 16:53:22 +0200392
Harald Weltec91085e2022-02-10 18:05:45 +0100393 # Make sure that the script file exists and that it is readable.
394 if not os.access(opts.script_path, os.R_OK):
395 self.poutput("Invalid script file!")
396 return
Philipp Maier76667642021-09-22 16:53:22 +0200397
Harald Weltec91085e2022-02-10 18:05:45 +0100398 success_count = 0
399 fail_count = 0
Philipp Maier76667642021-09-22 16:53:22 +0200400
Harald Weltec91085e2022-02-10 18:05:45 +0100401 first = True
402 while 1:
403 # TODO: Count consecutive failures, if more than N consecutive failures occur, then stop.
404 # The ratinale is: There may be a problem with the device, we do want to prevent that
405 # all remaining cards are fired to the error bin. This is only relevant for situations
406 # with large stacks, probably we do not need this feature right now.
Philipp Maier76667642021-09-22 16:53:22 +0200407
Harald Weltec91085e2022-02-10 18:05:45 +0100408 try:
409 # In case of failure, try multiple times.
410 for i in range(opts.tries):
411 # fetch card into reader bay
412 ch.get(first)
Philipp Maier76667642021-09-22 16:53:22 +0200413
Harald Weltec91085e2022-02-10 18:05:45 +0100414 # if necessary execute an action before we start processing the card
415 if(opts.pre_card_action):
416 os.system(opts.pre_card_action)
Philipp Maier76667642021-09-22 16:53:22 +0200417
Harald Weltec91085e2022-02-10 18:05:45 +0100418 # process the card
419 rc = self._process_card(first, opts.script_path)
420 if rc == 0:
421 success_count = success_count + 1
422 self._show_success_sign()
423 self.poutput("Statistics: success :%i, failure: %i" % (
424 success_count, fail_count))
425 break
426 else:
427 fail_count = fail_count + 1
428 self._show_failure_sign()
429 self.poutput("Statistics: success :%i, failure: %i" % (
430 success_count, fail_count))
Philipp Maier76667642021-09-22 16:53:22 +0200431
Harald Weltec91085e2022-02-10 18:05:45 +0100432 # Depending on success or failure, the card goes either in the "error" bin or in the
433 # "done" bin.
434 if rc < 0:
435 ch.error()
436 else:
437 ch.done()
Philipp Maier76667642021-09-22 16:53:22 +0200438
Harald Weltec91085e2022-02-10 18:05:45 +0100439 # In most cases it is possible to proceed with the next card, but the
440 # user may decide to halt immediately when an error occurs
441 if opts.halt_on_error and rc < 0:
442 return
Philipp Maier76667642021-09-22 16:53:22 +0200443
Harald Weltec91085e2022-02-10 18:05:45 +0100444 except (KeyboardInterrupt):
445 self.poutput("")
446 self.poutput("Terminated by user!")
447 return
448 except (SystemExit):
449 # When all cards are processed the card handler device will throw a SystemExit
450 # exception. Also Errors that are not recoverable (cards stuck etc.) will end up here.
451 # The user has the option to execute some action to make aware that the card handler
452 # needs service.
453 if(opts.on_stop_action):
454 os.system(opts.on_stop_action)
455 return
456 except:
457 self.poutput("")
458 self.poutput("Card handling failed with an exception:")
459 self.poutput("---------------------8<---------------------")
460 traceback.print_exc()
461 self.poutput("---------------------8<---------------------")
462 self.poutput("")
463 fail_count = fail_count + 1
464 self._show_failure_sign()
465 self.poutput("Statistics: success :%i, failure: %i" %
466 (success_count, fail_count))
Philipp Maierb52feed2021-09-22 16:43:13 +0200467
Harald Weltec91085e2022-02-10 18:05:45 +0100468 first = False
469
470 echo_parser = argparse.ArgumentParser()
471 echo_parser.add_argument('string', help="string to echo on the shell")
472
473 @cmd2.with_argparser(echo_parser)
474 @cmd2.with_category(CUSTOM_CATEGORY)
475 def do_echo(self, opts):
476 """Echo (print) a string on the console"""
477 self.poutput(opts.string)
478
Harald Weltefc315482022-07-23 12:49:14 +0200479 @cmd2.with_category(CUSTOM_CATEGORY)
480 def do_version(self, opts):
481 """Print the pySim software version."""
482 import pkg_resources
483 self.poutput(pkg_resources.get_distribution('pySim'))
Harald Welteb2edd142021-01-08 23:29:35 +0100484
Harald Welte31d2cf02021-04-03 10:47:29 +0200485@with_default_category('pySim Commands')
486class PySimCommands(CommandSet):
Harald Weltec91085e2022-02-10 18:05:45 +0100487 def __init__(self):
488 super().__init__()
Harald Welteb2edd142021-01-08 23:29:35 +0100489
Harald Weltec91085e2022-02-10 18:05:45 +0100490 dir_parser = argparse.ArgumentParser()
491 dir_parser.add_argument(
492 '--fids', help='Show file identifiers', action='store_true')
493 dir_parser.add_argument(
494 '--names', help='Show file names', action='store_true')
495 dir_parser.add_argument(
496 '--apps', help='Show applications', action='store_true')
497 dir_parser.add_argument(
498 '--all', help='Show all selectable identifiers and names', action='store_true')
Philipp Maier5d3e2592021-02-22 17:22:16 +0100499
Harald Weltec91085e2022-02-10 18:05:45 +0100500 @cmd2.with_argparser(dir_parser)
501 def do_dir(self, opts):
502 """Show a listing of files available in currently selected DF or MF"""
503 if opts.all:
504 flags = []
505 elif opts.fids or opts.names or opts.apps:
506 flags = ['PARENT', 'SELF']
507 if opts.fids:
508 flags += ['FIDS', 'AIDS']
509 if opts.names:
510 flags += ['FNAMES', 'ANAMES']
511 if opts.apps:
512 flags += ['ANAMES', 'AIDS']
513 else:
514 flags = ['PARENT', 'SELF', 'FNAMES', 'ANAMES']
515 selectables = list(
Harald Weltea6c0f882022-07-17 14:23:17 +0200516 self._cmd.lchan.selected_file.get_selectable_names(flags=flags))
Harald Weltec91085e2022-02-10 18:05:45 +0100517 directory_str = tabulate_str_list(
518 selectables, width=79, hspace=2, lspace=1, align_left=True)
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200519 path = self._cmd.lchan.selected_file.fully_qualified_path_str(True)
520 self._cmd.poutput(path)
521 path = self._cmd.lchan.selected_file.fully_qualified_path_str(False)
522 self._cmd.poutput(path)
Harald Weltec91085e2022-02-10 18:05:45 +0100523 self._cmd.poutput(directory_str)
524 self._cmd.poutput("%d files" % len(selectables))
Harald Welteb2edd142021-01-08 23:29:35 +0100525
Philipp Maier7b138b02022-05-31 13:42:56 +0200526 def walk(self, indent=0, action_ef=None, action_df=None, context=None, **kwargs):
Harald Weltec91085e2022-02-10 18:05:45 +0100527 """Recursively walk through the file system, starting at the currently selected DF"""
Philipp Maier7b138b02022-05-31 13:42:56 +0200528
Harald Weltea6c0f882022-07-17 14:23:17 +0200529 if isinstance(self._cmd.lchan.selected_file, CardDF):
Philipp Maier7b138b02022-05-31 13:42:56 +0200530 if action_df:
531 action_df(context, opts)
532
Harald Weltea6c0f882022-07-17 14:23:17 +0200533 files = self._cmd.lchan.selected_file.get_selectables(
Harald Weltec91085e2022-02-10 18:05:45 +0100534 flags=['FNAMES', 'ANAMES'])
535 for f in files:
Philipp Maier7b138b02022-05-31 13:42:56 +0200536 # special case: When no action is performed, just output a directory
537 if not action_ef and not action_df:
Harald Weltec91085e2022-02-10 18:05:45 +0100538 output_str = " " * indent + str(f) + (" " * 250)
539 output_str = output_str[0:25]
540 if isinstance(files[f], CardADF):
541 output_str += " " + str(files[f].aid)
542 else:
543 output_str += " " + str(files[f].fid)
544 output_str += " " + str(files[f].desc)
545 self._cmd.poutput(output_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200546
Harald Weltec91085e2022-02-10 18:05:45 +0100547 if isinstance(files[f], CardDF):
548 skip_df = False
549 try:
Harald Weltea6c0f882022-07-17 14:23:17 +0200550 fcp_dec = self._cmd.lchan.select(f, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100551 except Exception as e:
552 skip_df = True
Harald Weltea6c0f882022-07-17 14:23:17 +0200553 df = self._cmd.lchan.selected_file
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200554 df_path = df.fully_qualified_path_str(True)
555 df_skip_reason_str = df_path + \
Harald Weltec91085e2022-02-10 18:05:45 +0100556 "/" + str(f) + ", " + str(e)
557 if context:
558 context['DF_SKIP'] += 1
559 context['DF_SKIP_REASON'].append(df_skip_reason_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200560
Harald Weltec91085e2022-02-10 18:05:45 +0100561 # If the DF was skipped, we never have entered the directory
562 # below, so we must not move up.
563 if skip_df == False:
Philipp Maier7b138b02022-05-31 13:42:56 +0200564 self.walk(indent + 1, action_ef, action_df, context, **kwargs)
Harald Weltea6c0f882022-07-17 14:23:17 +0200565 fcp_dec = self._cmd.lchan.select("..", self._cmd)
Philipp Maierf408a402021-04-09 21:16:12 +0200566
Philipp Maier7b138b02022-05-31 13:42:56 +0200567 elif action_ef:
Harald Weltea6c0f882022-07-17 14:23:17 +0200568 df_before_action = self._cmd.lchan.selected_file
Philipp Maier7b138b02022-05-31 13:42:56 +0200569 action_ef(f, context, **kwargs)
Harald Weltec91085e2022-02-10 18:05:45 +0100570 # When walking through the file system tree the action must not
571 # always restore the currently selected file to the file that
572 # was selected before executing the action() callback.
Harald Weltea6c0f882022-07-17 14:23:17 +0200573 if df_before_action != self._cmd.lchan.selected_file:
Harald Weltec91085e2022-02-10 18:05:45 +0100574 raise RuntimeError("inconsistent walk, %s is currently selected but expecting %s to be selected"
Harald Weltea6c0f882022-07-17 14:23:17 +0200575 % (str(self._cmd.lchan.selected_file), str(df_before_action)))
Philipp Maierff9dae22021-02-25 17:03:21 +0100576
Harald Weltec91085e2022-02-10 18:05:45 +0100577 def do_tree(self, opts):
578 """Display a filesystem-tree with all selectable files"""
579 self.walk()
Philipp Maierff9dae22021-02-25 17:03:21 +0100580
Philipp Maier7b138b02022-05-31 13:42:56 +0200581 def export_ef(self, filename, context, as_json):
582 """ Select and export a single elementary file (EF) """
Harald Weltec91085e2022-02-10 18:05:45 +0100583 context['COUNT'] += 1
Harald Weltea6c0f882022-07-17 14:23:17 +0200584 df = self._cmd.lchan.selected_file
Philipp Maierac34dcc2021-04-01 17:19:05 +0200585
Philipp Maierea81f752022-05-19 10:13:30 +0200586 # The currently selected file (not the file we are going to export)
587 # must always be an ADF or DF. From this starting point we select
588 # the EF we want to export. To maintain consistency we will then
589 # select the current DF again (see comment below).
Harald Weltec91085e2022-02-10 18:05:45 +0100590 if not isinstance(df, CardDF):
591 raise RuntimeError(
592 "currently selected file %s is not a DF or ADF" % str(df))
Philipp Maierac34dcc2021-04-01 17:19:05 +0200593
Harald Weltec91085e2022-02-10 18:05:45 +0100594 df_path_list = df.fully_qualified_path(True)
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200595 df_path = df.fully_qualified_path_str(True)
596 df_path_fid = df.fully_qualified_path_str(False)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100597
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200598 file_str = df_path + "/" + str(filename)
Harald Weltec91085e2022-02-10 18:05:45 +0100599 self._cmd.poutput(boxed_heading_str(file_str))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100600
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200601 self._cmd.poutput("# directory: %s (%s)" % (df_path, df_path_fid))
Harald Weltec91085e2022-02-10 18:05:45 +0100602 try:
Harald Weltea6c0f882022-07-17 14:23:17 +0200603 fcp_dec = self._cmd.lchan.select(filename, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100604 self._cmd.poutput("# file: %s (%s)" % (
Harald Weltea6c0f882022-07-17 14:23:17 +0200605 self._cmd.lchan.selected_file.name, self._cmd.lchan.selected_file.fid))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100606
Harald Weltea6c0f882022-07-17 14:23:17 +0200607 structure = self._cmd.lchan.selected_file_structure()
Harald Weltec91085e2022-02-10 18:05:45 +0100608 self._cmd.poutput("# structure: %s" % str(structure))
Harald Weltea6c0f882022-07-17 14:23:17 +0200609 self._cmd.poutput("# RAW FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp_hex))
610 self._cmd.poutput("# Decoded FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100611
Harald Weltec91085e2022-02-10 18:05:45 +0100612 for f in df_path_list:
613 self._cmd.poutput("select " + str(f))
Harald Weltea6c0f882022-07-17 14:23:17 +0200614 self._cmd.poutput("select " + self._cmd.lchan.selected_file.name)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100615
Harald Weltec91085e2022-02-10 18:05:45 +0100616 if structure == 'transparent':
Harald Welte08b11ab2022-02-10 18:56:41 +0100617 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200618 result = self._cmd.lchan.read_binary_dec()
Harald Welte08b11ab2022-02-10 18:56:41 +0100619 self._cmd.poutput("update_binary_decoded '%s'" % json.dumps(result[0], cls=JsonEncoder))
620 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200621 result = self._cmd.lchan.read_binary()
Harald Welte08b11ab2022-02-10 18:56:41 +0100622 self._cmd.poutput("update_binary " + str(result[0]))
Harald Weltec91085e2022-02-10 18:05:45 +0100623 elif structure == 'cyclic' or structure == 'linear_fixed':
624 # Use number of records specified in select response
Harald Weltea6c0f882022-07-17 14:23:17 +0200625 num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
Harald Welte747a9782022-02-13 17:52:28 +0100626 if num_of_rec:
Harald Weltec91085e2022-02-10 18:05:45 +0100627 for r in range(1, num_of_rec + 1):
Harald Welte08b11ab2022-02-10 18:56:41 +0100628 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200629 result = self._cmd.lchan.read_record_dec(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100630 self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
631 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200632 result = self._cmd.lchan.read_record(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100633 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
634
Harald Weltec91085e2022-02-10 18:05:45 +0100635 # When the select response does not return the number of records, read until we hit the
636 # first record that cannot be read.
637 else:
638 r = 1
639 while True:
640 try:
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])))
Harald Weltec91085e2022-02-10 18:05:45 +0100647 except SwMatchError as e:
648 # We are past the last valid record - stop
649 if e.sw_actual == "9402":
650 break
651 # Some other problem occurred
652 else:
653 raise e
Harald Weltec91085e2022-02-10 18:05:45 +0100654 r = r + 1
655 elif structure == 'ber_tlv':
Harald Weltea6c0f882022-07-17 14:23:17 +0200656 tags = self._cmd.lchan.retrieve_tags()
Harald Weltec91085e2022-02-10 18:05:45 +0100657 for t in tags:
Harald Weltea6c0f882022-07-17 14:23:17 +0200658 result = self._cmd.lchan.retrieve_data(t)
Harald Weltec91085e2022-02-10 18:05:45 +0100659 (tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
660 self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val)))
661 else:
662 raise RuntimeError(
663 'Unsupported structure "%s" of file "%s"' % (structure, filename))
664 except Exception as e:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200665 bad_file_str = df_path + "/" + str(filename) + ", " + str(e)
Harald Weltec91085e2022-02-10 18:05:45 +0100666 self._cmd.poutput("# bad file: %s" % bad_file_str)
667 context['ERR'] += 1
668 context['BAD'].append(bad_file_str)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100669
Harald Weltec91085e2022-02-10 18:05:45 +0100670 # When reading the file is done, make sure the parent file is
671 # selected again. This will be the usual case, however we need
672 # to check before since we must not select the same DF twice
Harald Weltea6c0f882022-07-17 14:23:17 +0200673 if df != self._cmd.lchan.selected_file:
674 self._cmd.lchan.select(df.fid or df.aid, self._cmd)
Philipp Maierac34dcc2021-04-01 17:19:05 +0200675
Harald Weltec91085e2022-02-10 18:05:45 +0100676 self._cmd.poutput("#")
Philipp Maier24f7bd32021-02-25 17:06:18 +0100677
Harald Weltec91085e2022-02-10 18:05:45 +0100678 export_parser = argparse.ArgumentParser()
679 export_parser.add_argument(
680 '--filename', type=str, default=None, help='only export specific file')
Harald Welte08b11ab2022-02-10 18:56:41 +0100681 export_parser.add_argument(
682 '--json', action='store_true', help='export as JSON (less reliable)')
Philipp Maier24f7bd32021-02-25 17:06:18 +0100683
Harald Weltec91085e2022-02-10 18:05:45 +0100684 @cmd2.with_argparser(export_parser)
685 def do_export(self, opts):
686 """Export files to script that can be imported back later"""
687 context = {'ERR': 0, 'COUNT': 0, 'BAD': [],
688 'DF_SKIP': 0, 'DF_SKIP_REASON': []}
Philipp Maier9a4091d2022-05-19 10:20:30 +0200689 kwargs_export = {'as_json': opts.json}
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200690 exception_str_add = ""
691
Harald Weltec91085e2022-02-10 18:05:45 +0100692 if opts.filename:
Philipp Maier7b138b02022-05-31 13:42:56 +0200693 self.export_ef(opts.filename, context, **kwargs_export)
Harald Weltec91085e2022-02-10 18:05:45 +0100694 else:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200695 try:
696 self.walk(0, self.export_ef, None, context, **kwargs_export)
697 except Exception as e:
698 print("# Stopping early here due to exception: " + str(e))
699 print("#")
700 exception_str_add = ", also had to stop early due to exception:" + str(e)
Philipp Maier80ce71f2021-04-19 21:24:23 +0200701
Harald Weltec91085e2022-02-10 18:05:45 +0100702 self._cmd.poutput(boxed_heading_str("Export summary"))
Philipp Maier80ce71f2021-04-19 21:24:23 +0200703
Harald Weltec91085e2022-02-10 18:05:45 +0100704 self._cmd.poutput("# total files visited: %u" % context['COUNT'])
705 self._cmd.poutput("# bad files: %u" % context['ERR'])
706 for b in context['BAD']:
707 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200708
Harald Weltec91085e2022-02-10 18:05:45 +0100709 self._cmd.poutput("# skipped dedicated files(s): %u" %
710 context['DF_SKIP'])
711 for b in context['DF_SKIP_REASON']:
712 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200713
Harald Weltec91085e2022-02-10 18:05:45 +0100714 if context['ERR'] and context['DF_SKIP']:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200715 raise RuntimeError("unable to export %i elementary file(s) and %i dedicated file(s)%s" % (
716 context['ERR'], context['DF_SKIP'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100717 elif context['ERR']:
718 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200719 "unable to export %i elementary file(s)%s" % (context['ERR'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100720 elif context['DF_SKIP']:
721 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200722 "unable to export %i dedicated files(s)%s" % (context['ERR'], exception_str_add))
Harald Welteb2edd142021-01-08 23:29:35 +0100723
Harald Weltec91085e2022-02-10 18:05:45 +0100724 def do_reset(self, opts):
725 """Reset the Card."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200726 atr = self._cmd.lchan.reset(self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100727 self._cmd.poutput('Card ATR: %s' % atr)
728 self._cmd.update_prompt()
Harald Weltedaf2b392021-05-03 23:17:29 +0200729
Harald Weltec91085e2022-02-10 18:05:45 +0100730 def do_desc(self, opts):
731 """Display human readable file description for the currently selected file"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200732 desc = self._cmd.lchan.selected_file.desc
Harald Weltec91085e2022-02-10 18:05:45 +0100733 if desc:
734 self._cmd.poutput(desc)
735 else:
736 self._cmd.poutput("no description available")
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200737
Harald Weltec91085e2022-02-10 18:05:45 +0100738 def do_verify_adm(self, arg):
739 """VERIFY the ADM1 PIN"""
740 if arg:
741 # use specified ADM-PIN
742 pin_adm = sanitize_pin_adm(arg)
743 else:
744 # try to find an ADM-PIN if none is specified
745 result = card_key_provider_get_field(
746 'ADM1', key='ICCID', value=self._cmd.iccid)
747 pin_adm = sanitize_pin_adm(result)
748 if pin_adm:
749 self._cmd.poutput(
750 "found ADM-PIN '%s' for ICCID '%s'" % (result, self._cmd.iccid))
751 else:
752 raise ValueError(
753 "cannot find ADM-PIN for ICCID '%s'" % (self._cmd.iccid))
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200754
Harald Weltec91085e2022-02-10 18:05:45 +0100755 if pin_adm:
756 self._cmd.card.verify_adm(h2b(pin_adm))
757 else:
758 raise ValueError("error: cannot authenticate, no adm-pin!")
759
Philipp Maier7b9e2442023-03-22 15:19:54 +0100760 def do_cardinfo(self, opts):
761 """Display information about the currently inserted card"""
762 self._cmd.poutput("Card info:")
763 self._cmd.poutput(" Name: %s" % self._cmd.card.name)
764 self._cmd.poutput(" ATR: %s" % b2h(self._cmd.card._scc.get_atr()))
765 self._cmd.poutput(" ICCID: %s" % self._cmd.iccid)
766 self._cmd.poutput(" Class-Byte: %s" % self._cmd.card._scc.cla_byte)
767 self._cmd.poutput(" Select-Ctrl: %s" % self._cmd.card._scc.sel_ctrl)
768 self._cmd.poutput(" AIDs:")
769 for a in self._cmd.rs.mf.applications:
770 self._cmd.poutput(" %s" % a)
Harald Welteb2edd142021-01-08 23:29:35 +0100771
Harald Welte31d2cf02021-04-03 10:47:29 +0200772@with_default_category('ISO7816 Commands')
773class Iso7816Commands(CommandSet):
Harald Weltec91085e2022-02-10 18:05:45 +0100774 def __init__(self):
775 super().__init__()
Harald Welte31d2cf02021-04-03 10:47:29 +0200776
Harald Weltec91085e2022-02-10 18:05:45 +0100777 def do_select(self, opts):
778 """SELECT a File (ADF/DF/EF)"""
779 if len(opts.arg_list) == 0:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200780 path = self._cmd.lchan.selected_file.fully_qualified_path_str(True)
781 path_fid = self._cmd.lchan.selected_file.fully_qualified_path_str(False)
782 self._cmd.poutput("currently selected file: %s (%s)" % (path, path_fid))
Harald Weltec91085e2022-02-10 18:05:45 +0100783 return
Harald Welte31d2cf02021-04-03 10:47:29 +0200784
Harald Weltec91085e2022-02-10 18:05:45 +0100785 path = opts.arg_list[0]
Harald Weltea6c0f882022-07-17 14:23:17 +0200786 fcp_dec = self._cmd.lchan.select(path, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100787 self._cmd.update_prompt()
788 self._cmd.poutput_json(fcp_dec)
Harald Welte31d2cf02021-04-03 10:47:29 +0200789
Harald Weltec91085e2022-02-10 18:05:45 +0100790 def complete_select(self, text, line, begidx, endidx) -> List[str]:
791 """Command Line tab completion for SELECT"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200792 index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
Harald Weltec91085e2022-02-10 18:05:45 +0100793 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200794
Harald Weltec91085e2022-02-10 18:05:45 +0100795 def get_code(self, code):
796 """Use code either directly or try to get it from external data source"""
797 auto = ('PIN1', 'PIN2', 'PUK1', 'PUK2')
Harald Welte31d2cf02021-04-03 10:47:29 +0200798
Harald Weltec91085e2022-02-10 18:05:45 +0100799 if str(code).upper() not in auto:
800 return sanitize_pin_adm(code)
Harald Welte31d2cf02021-04-03 10:47:29 +0200801
Harald Weltec91085e2022-02-10 18:05:45 +0100802 result = card_key_provider_get_field(
803 str(code), key='ICCID', value=self._cmd.iccid)
804 result = sanitize_pin_adm(result)
805 if result:
806 self._cmd.poutput("found %s '%s' for ICCID '%s'" %
807 (code.upper(), result, self._cmd.iccid))
808 else:
809 self._cmd.poutput("cannot find %s for ICCID '%s'" %
810 (code.upper(), self._cmd.iccid))
811 return result
Harald Welte31d2cf02021-04-03 10:47:29 +0200812
Harald Weltec91085e2022-02-10 18:05:45 +0100813 verify_chv_parser = argparse.ArgumentParser()
814 verify_chv_parser.add_argument(
815 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
816 verify_chv_parser.add_argument(
817 '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 +0200818
Harald Weltec91085e2022-02-10 18:05:45 +0100819 @cmd2.with_argparser(verify_chv_parser)
820 def do_verify_chv(self, opts):
Harald Welte12af7932022-02-15 16:39:08 +0100821 """Verify (authenticate) using specified CHV (PIN) code, which is how the specifications
822 call it if you authenticate yourself using the specified PIN. There usually is at least PIN1 and
823 PIN2."""
Harald Weltec91085e2022-02-10 18:05:45 +0100824 pin = self.get_code(opts.pin_code)
825 (data, sw) = self._cmd.card._scc.verify_chv(opts.pin_nr, h2b(pin))
826 self._cmd.poutput("CHV verification successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200827
Harald Weltec91085e2022-02-10 18:05:45 +0100828 unblock_chv_parser = argparse.ArgumentParser()
829 unblock_chv_parser.add_argument(
830 '--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
831 unblock_chv_parser.add_argument(
832 'puk_code', type=str, help='PUK code digits \"PUK1\" or \"PUK2\" to get PUK code from external data source')
833 unblock_chv_parser.add_argument(
834 '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 +0200835
Harald Weltec91085e2022-02-10 18:05:45 +0100836 @cmd2.with_argparser(unblock_chv_parser)
837 def do_unblock_chv(self, opts):
838 """Unblock PIN code using specified PUK code"""
839 new_pin = self.get_code(opts.new_pin_code)
840 puk = self.get_code(opts.puk_code)
841 (data, sw) = self._cmd.card._scc.unblock_chv(
842 opts.pin_nr, h2b(puk), h2b(new_pin))
843 self._cmd.poutput("CHV unblock successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200844
Harald Weltec91085e2022-02-10 18:05:45 +0100845 change_chv_parser = argparse.ArgumentParser()
846 change_chv_parser.add_argument(
847 '--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
848 change_chv_parser.add_argument(
849 'pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
850 change_chv_parser.add_argument(
851 '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 +0200852
Harald Weltec91085e2022-02-10 18:05:45 +0100853 @cmd2.with_argparser(change_chv_parser)
854 def do_change_chv(self, opts):
855 """Change PIN code to a new PIN code"""
856 new_pin = self.get_code(opts.new_pin_code)
857 pin = self.get_code(opts.pin_code)
858 (data, sw) = self._cmd.card._scc.change_chv(
859 opts.pin_nr, h2b(pin), h2b(new_pin))
860 self._cmd.poutput("CHV change successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200861
Harald Weltec91085e2022-02-10 18:05:45 +0100862 disable_chv_parser = argparse.ArgumentParser()
863 disable_chv_parser.add_argument(
864 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
865 disable_chv_parser.add_argument(
866 '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 +0200867
Harald Weltec91085e2022-02-10 18:05:45 +0100868 @cmd2.with_argparser(disable_chv_parser)
869 def do_disable_chv(self, opts):
870 """Disable PIN code using specified PIN code"""
871 pin = self.get_code(opts.pin_code)
872 (data, sw) = self._cmd.card._scc.disable_chv(opts.pin_nr, h2b(pin))
873 self._cmd.poutput("CHV disable successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200874
Harald Weltec91085e2022-02-10 18:05:45 +0100875 enable_chv_parser = argparse.ArgumentParser()
876 enable_chv_parser.add_argument(
877 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
878 enable_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(enable_chv_parser)
882 def do_enable_chv(self, opts):
883 """Enable PIN code using specified PIN code"""
884 pin = self.get_code(opts.pin_code)
885 (data, sw) = self._cmd.card._scc.enable_chv(opts.pin_nr, h2b(pin))
886 self._cmd.poutput("CHV enable successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200887
Harald Weltec91085e2022-02-10 18:05:45 +0100888 def do_deactivate_file(self, opts):
Harald Welte799c3542022-02-15 15:56:28 +0100889 """Deactivate the currently selected EF"""
Harald Weltec91085e2022-02-10 18:05:45 +0100890 (data, sw) = self._cmd.card._scc.deactivate_file()
Harald Weltea4631612021-04-10 18:17:55 +0200891
Harald Welte799c3542022-02-15 15:56:28 +0100892 activate_file_parser = argparse.ArgumentParser()
893 activate_file_parser.add_argument('NAME', type=str, help='File name or FID of file to activate')
894 @cmd2.with_argparser(activate_file_parser)
Harald Weltec91085e2022-02-10 18:05:45 +0100895 def do_activate_file(self, opts):
Harald Welte12af7932022-02-15 16:39:08 +0100896 """Activate the specified EF. This used to be called REHABILITATE in TS 11.11 for classic
897 SIM. You need to specify the name or FID of the file to activate."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200898 (data, sw) = self._cmd.lchan.activate_file(opts.NAME)
Harald Welte485692b2021-05-25 22:21:44 +0200899
Harald Weltec91085e2022-02-10 18:05:45 +0100900 def complete_activate_file(self, text, line, begidx, endidx) -> List[str]:
901 """Command Line tab completion for ACTIVATE FILE"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200902 index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
Harald Weltec91085e2022-02-10 18:05:45 +0100903 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200904
Harald Weltec91085e2022-02-10 18:05:45 +0100905 open_chan_parser = argparse.ArgumentParser()
906 open_chan_parser.add_argument(
907 'chan_nr', type=int, default=0, help='Channel Number')
Harald Welte703f9332021-04-10 18:39:32 +0200908
Harald Weltec91085e2022-02-10 18:05:45 +0100909 @cmd2.with_argparser(open_chan_parser)
910 def do_open_channel(self, opts):
911 """Open a logical channel."""
912 (data, sw) = self._cmd.card._scc.manage_channel(
913 mode='open', lchan_nr=opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200914
Harald Weltec91085e2022-02-10 18:05:45 +0100915 close_chan_parser = argparse.ArgumentParser()
916 close_chan_parser.add_argument(
917 'chan_nr', type=int, default=0, help='Channel Number')
Harald Welte703f9332021-04-10 18:39:32 +0200918
Harald Weltec91085e2022-02-10 18:05:45 +0100919 @cmd2.with_argparser(close_chan_parser)
920 def do_close_channel(self, opts):
921 """Close a logical channel."""
922 (data, sw) = self._cmd.card._scc.manage_channel(
923 mode='close', lchan_nr=opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200924
Harald Weltec91085e2022-02-10 18:05:45 +0100925 def do_status(self, opts):
926 """Perform the STATUS command."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200927 fcp_dec = self._cmd.lchan.status()
Harald Weltec91085e2022-02-10 18:05:45 +0100928 self._cmd.poutput_json(fcp_dec)
Harald Welte34b05d32021-05-25 22:03:13 +0200929
Harald Weltec91085e2022-02-10 18:05:45 +0100930 suspend_uicc_parser = argparse.ArgumentParser()
931 suspend_uicc_parser.add_argument('--min-duration-secs', type=int, default=60,
932 help='Proposed minimum duration of suspension')
933 suspend_uicc_parser.add_argument('--max-duration-secs', type=int, default=24*60*60,
934 help='Proposed maximum duration of suspension')
Harald Welteec950532021-10-20 13:09:00 +0200935
Harald Weltec91085e2022-02-10 18:05:45 +0100936 # not ISO7816-4 but TS 102 221
937 @cmd2.with_argparser(suspend_uicc_parser)
938 def do_suspend_uicc(self, opts):
939 """Perform the SUSPEND UICC command. Only supported on some UICC."""
940 (duration, token, sw) = self._cmd.card._scc.suspend_uicc(min_len_secs=opts.min_duration_secs,
941 max_len_secs=opts.max_duration_secs)
942 self._cmd.poutput(
943 'Negotiated Duration: %u secs, Token: %s, SW: %s' % (duration, token, sw))
Harald Welteec950532021-10-20 13:09:00 +0200944
Harald Weltecab26c72022-08-06 16:12:30 +0200945class Proact(ProactiveHandler):
946 def receive_fetch(self, pcmd: ProactiveCommand):
947 # print its parsed representation
948 print(pcmd.decoded)
949 # TODO: implement the basics, such as SMS Sending, ...
950
951
Harald Welte703f9332021-04-10 18:39:32 +0200952
Harald Weltef2e761c2021-04-11 11:56:44 +0200953option_parser = argparse.ArgumentParser(prog='pySim-shell', description='interactive SIM card shell',
954 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
Harald Welte28c24312021-04-11 12:19:36 +0200955argparse_add_reader_args(option_parser)
Harald Weltec8ff0262021-04-11 12:06:13 +0200956
957global_group = option_parser.add_argument_group('General Options')
958global_group.add_argument('--script', metavar='PATH', default=None,
Harald Welte28c24312021-04-11 12:19:36 +0200959 help='script with pySim-shell commands to be executed automatically at start-up')
Harald Weltec91085e2022-02-10 18:05:45 +0100960global_group.add_argument('--csv', metavar='FILE',
961 default=None, help='Read card data from CSV file')
Philipp Maier76667642021-09-22 16:53:22 +0200962global_group.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
Harald Weltec91085e2022-02-10 18:05:45 +0100963 help="Use automatic card handling machine")
Harald Weltec8ff0262021-04-11 12:06:13 +0200964
965adm_group = global_group.add_mutually_exclusive_group()
966adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
967 help='ADM PIN used for provisioning (overwrites default)')
968adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM1_HEX', dest='pin_adm_hex', default=None,
969 help='ADM PIN used for provisioning, as hex string (16 characters long)')
Harald Welteb2edd142021-01-08 23:29:35 +0100970
971
972if __name__ == '__main__':
973
Harald Weltec91085e2022-02-10 18:05:45 +0100974 # Parse options
975 opts = option_parser.parse_args()
Harald Welteb2edd142021-01-08 23:29:35 +0100976
Harald Weltec91085e2022-02-10 18:05:45 +0100977 # If a script file is specified, be sure that it actually exists
978 if opts.script:
979 if not os.access(opts.script, os.R_OK):
980 print("Invalid script file!")
981 sys.exit(2)
Philipp Maier13e258d2021-04-08 17:48:49 +0200982
Harald Weltec91085e2022-02-10 18:05:45 +0100983 # Register csv-file as card data provider, either from specified CSV
984 # or from CSV file in home directory
985 csv_default = str(Path.home()) + "/.osmocom/pysim/card_data.csv"
986 if opts.csv:
987 card_key_provider_register(CardKeyProviderCsv(opts.csv))
988 if os.path.isfile(csv_default):
989 card_key_provider_register(CardKeyProviderCsv(csv_default))
Philipp Maier2b11c322021-03-17 12:37:39 +0100990
Harald Weltec91085e2022-02-10 18:05:45 +0100991 # Init card reader driver
Harald Weltecab26c72022-08-06 16:12:30 +0200992 sl = init_reader(opts, proactive_handler = Proact())
Harald Weltec91085e2022-02-10 18:05:45 +0100993 if sl is None:
994 exit(1)
Philipp Maierea95c392021-09-16 13:10:19 +0200995
Harald Weltec91085e2022-02-10 18:05:45 +0100996 # Create command layer
997 scc = SimCardCommands(transport=sl)
Philipp Maierea95c392021-09-16 13:10:19 +0200998
Harald Weltec91085e2022-02-10 18:05:45 +0100999 # Create a card handler (for bulk provisioning)
1000 if opts.card_handler_config:
1001 ch = CardHandlerAuto(None, opts.card_handler_config)
1002 else:
1003 ch = CardHandler(sl)
Philipp Maier76667642021-09-22 16:53:22 +02001004
Harald Weltec91085e2022-02-10 18:05:45 +01001005 # Detect and initialize the card in the reader. This may fail when there
1006 # is no card in the reader or the card is unresponsive. PysimApp is
1007 # able to tolerate and recover from that.
1008 try:
1009 rs, card = init_card(sl)
1010 app = PysimApp(card, rs, sl, ch, opts.script)
1011 except:
1012 print("Card initialization failed with an exception:")
1013 print("---------------------8<---------------------")
1014 traceback.print_exc()
1015 print("---------------------8<---------------------")
1016 print("(you may still try to recover from this manually by using the 'equip' command.)")
1017 print(
1018 " it should also be noted that some readers may behave strangely when no card")
1019 print(" is inserted.)")
1020 print("")
Philipp Maier7226c092022-06-01 17:58:38 +02001021 app = PysimApp(card, None, sl, ch, opts.script)
Philipp Maierea95c392021-09-16 13:10:19 +02001022
Harald Weltec91085e2022-02-10 18:05:45 +01001023 # If the user supplies an ADM PIN at via commandline args authenticate
1024 # immediately so that the user does not have to use the shell commands
1025 pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
1026 if pin_adm:
1027 if not card:
1028 print("Card error, cannot do ADM verification with supplied ADM pin now.")
1029 try:
1030 card.verify_adm(h2b(pin_adm))
1031 except Exception as e:
1032 print(e)
Philipp Maier228c98e2021-03-10 20:14:06 +01001033
Harald Weltec91085e2022-02-10 18:05:45 +01001034 app.cmdloop()