blob: ce6efb8c980bccf5ea0904a0a45352e836da24cb [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#
5# (C) 2021 by Harald Welte <laforge@osmocom.org>
6#
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
Vadim Yanitskiy0d9f0882022-07-14 19:08:24 +070026from cmd2 import style, fg
Harald Welteb2edd142021-01-08 23:29:35 +010027from cmd2 import CommandSet, with_default_category, with_argparser
28import argparse
29
30import os
31import sys
Philipp Maier2b11c322021-03-17 12:37:39 +010032from pathlib import Path
Philipp Maier76667642021-09-22 16:53:22 +020033from io import StringIO
Harald Welteb2edd142021-01-08 23:29:35 +010034
Harald Welteb2edd142021-01-08 23:29:35 +010035from pySim.exceptions import *
36from pySim.commands import SimCardCommands
Harald Welte28c24312021-04-11 12:19:36 +020037from pySim.transport import init_reader, ApduTracer, argparse_add_reader_args
Philipp Maierbb73e512021-05-05 16:14:00 +020038from pySim.cards import card_detect, SimCard
Harald Welte1e52b0d2022-07-16 11:53:21 +020039from pySim.utils import h2b, swap_nibbles, rpad, b2h, JsonEncoder, bertlv_parse_one, sw_match
40from pySim.utils import sanitize_pin_adm, tabulate_str_list, boxed_heading_str
Philipp Maier76667642021-09-22 16:53:22 +020041from pySim.card_handler import CardHandler, CardHandlerAuto
Harald Welteb2edd142021-01-08 23:29:35 +010042
Harald Welte1e52b0d2022-07-16 11:53:21 +020043from pySim.filesystem import RuntimeState, CardDF, CardADF, CardModel
Philipp Maiera028c7d2021-11-08 16:12:03 +010044from pySim.profile import CardProfile
Harald Welteb2edd142021-01-08 23:29:35 +010045from pySim.ts_102_221 import CardProfileUICC
Harald Welte3c9b7842021-10-19 21:44:24 +020046from pySim.ts_102_222 import Ts102222Commands
Harald Welte5ce35242021-04-02 20:27:05 +020047from pySim.ts_31_102 import CardApplicationUSIM
48from pySim.ts_31_103 import CardApplicationISIM
Harald Welte95ce6b12021-10-20 18:40:54 +020049from pySim.ara_m import CardApplicationARAM
Harald Welte34eb5042022-02-21 17:19:28 +010050from pySim.global_platform import CardApplicationISD
Harald Welte2a33ad22021-10-20 10:14:18 +020051from pySim.gsm_r import DF_EIRENE
Harald Welteb2edd142021-01-08 23:29:35 +010052
Harald Welte4c1dca02021-10-14 17:48:25 +020053# we need to import this module so that the SysmocomSJA2 sub-class of
54# CardModel is created, which will add the ATR-based matching and
55# calling of SysmocomSJA2.add_files. See CardModel.apply_matching_models
Harald Weltef44256c2021-10-14 15:53:39 +020056import pySim.sysmocom_sja2
57
Harald Welte4442b3d2021-04-03 09:00:16 +020058from pySim.card_key_provider import CardKeyProviderCsv, card_key_provider_register, card_key_provider_get_field
Philipp Maier2b11c322021-03-17 12:37:39 +010059
Harald Weltec91085e2022-02-10 18:05:45 +010060
Philipp Maierea95c392021-09-16 13:10:19 +020061def init_card(sl):
Harald Weltec91085e2022-02-10 18:05:45 +010062 """
63 Detect card in reader and setup card profile and runtime state. This
64 function must be called at least once on startup. The card and runtime
65 state object (rs) is required for all pySim-shell commands.
66 """
Philipp Maierea95c392021-09-16 13:10:19 +020067
Harald Weltec91085e2022-02-10 18:05:45 +010068 # Wait up to three seconds for a card in reader and try to detect
69 # the card type.
70 print("Waiting for card...")
71 try:
72 sl.wait_for_card(3)
73 except NoCardError:
74 print("No card detected!")
75 return None, None
76 except:
77 print("Card not readable!")
78 return None, None
Philipp Maierea95c392021-09-16 13:10:19 +020079
Philipp Maier24031252022-06-14 16:16:42 +020080 generic_card = False
Harald Weltec91085e2022-02-10 18:05:45 +010081 card = card_detect("auto", scc)
82 if card is None:
83 print("Warning: Could not detect card type - assuming a generic card type...")
84 card = SimCard(scc)
Philipp Maier24031252022-06-14 16:16:42 +020085 generic_card = True
Philipp Maiera028c7d2021-11-08 16:12:03 +010086
Harald Weltec91085e2022-02-10 18:05:45 +010087 profile = CardProfile.pick(scc)
88 if profile is None:
89 print("Unsupported card type!")
Philipp Maier7226c092022-06-01 17:58:38 +020090 return None, card
Philipp Maierea95c392021-09-16 13:10:19 +020091
Philipp Maier24031252022-06-14 16:16:42 +020092 # ETSI TS 102 221, Table 9.3 specifies a default for the PIN key
93 # references, however card manufactures may still decide to pick an
94 # arbitrary key reference. In case we run on a generic card class that is
95 # detected as an UICC, we will pick the key reference that is officially
96 # specified.
97 if generic_card and isinstance(profile, CardProfileUICC):
98 card._adm_chv_num = 0x0A
99
Harald Weltec91085e2022-02-10 18:05:45 +0100100 print("Info: Card is of type: %s" % str(profile))
Philipp Maiera028c7d2021-11-08 16:12:03 +0100101
Harald Weltec91085e2022-02-10 18:05:45 +0100102 # FIXME: This shouln't be here, the profile should add the applications,
103 # however, we cannot simply put his into ts_102_221.py since we would
104 # have to e.g. import CardApplicationUSIM from ts_31_102.py, which already
105 # imports from ts_102_221.py. This means we will end up with a circular
106 # import, which needs to be resolved first.
107 if isinstance(profile, CardProfileUICC):
108 profile.add_application(CardApplicationUSIM())
109 profile.add_application(CardApplicationISIM())
110 profile.add_application(CardApplicationARAM())
Harald Welte34eb5042022-02-21 17:19:28 +0100111 profile.add_application(CardApplicationISD())
Philipp Maiera028c7d2021-11-08 16:12:03 +0100112
Harald Weltec91085e2022-02-10 18:05:45 +0100113 # Create runtime state with card profile
114 rs = RuntimeState(card, profile)
Philipp Maierea95c392021-09-16 13:10:19 +0200115
Harald Weltec91085e2022-02-10 18:05:45 +0100116 # FIXME: This is an GSM-R related file, it needs to be added throughout,
117 # the profile. At the moment we add it for all cards, this won't hurt,
118 # but regular SIM and UICC will not have it and fail to select it.
119 rs.mf.add_file(DF_EIRENE())
Philipp Maierea95c392021-09-16 13:10:19 +0200120
Harald Weltec91085e2022-02-10 18:05:45 +0100121 CardModel.apply_matching_models(scc, rs)
Harald Weltef44256c2021-10-14 15:53:39 +0200122
Harald Weltec91085e2022-02-10 18:05:45 +0100123 # inform the transport that we can do context-specific SW interpretation
124 sl.set_sw_interpreter(rs)
Philipp Maierea95c392021-09-16 13:10:19 +0200125
Harald Weltec91085e2022-02-10 18:05:45 +0100126 return rs, card
127
Philipp Maier2b11c322021-03-17 12:37:39 +0100128
Harald Welteb2edd142021-01-08 23:29:35 +0100129class PysimApp(cmd2.Cmd):
Harald Weltec91085e2022-02-10 18:05:45 +0100130 CUSTOM_CATEGORY = 'pySim Commands'
Philipp Maier76667642021-09-22 16:53:22 +0200131
Harald Weltec91085e2022-02-10 18:05:45 +0100132 def __init__(self, card, rs, sl, ch, script=None):
133 super().__init__(persistent_history_file='~/.pysim_shell_history', allow_cli_args=False,
134 use_ipython=True, auto_load_commands=False, startup_script=script)
135 self.intro = style('Welcome to pySim-shell!', fg=fg.red)
136 self.default_category = 'pySim-shell built-in commands'
137 self.card = None
138 self.rs = None
Harald Weltea6c0f882022-07-17 14:23:17 +0200139 self.lchan = None
140 self.py_locals = {'card': self.card, 'rs': self.rs, 'lchan': self.lchan}
Harald Weltec91085e2022-02-10 18:05:45 +0100141 self.sl = sl
142 self.ch = ch
Harald Welte1748b932021-04-06 21:12:25 +0200143
Harald Weltec91085e2022-02-10 18:05:45 +0100144 self.numeric_path = False
145 self.add_settable(cmd2.Settable('numeric_path', bool, 'Print File IDs instead of names',
146 onchange_cb=self._onchange_numeric_path))
147 self.conserve_write = True
148 self.add_settable(cmd2.Settable('conserve_write', bool, 'Read and compare before write',
149 onchange_cb=self._onchange_conserve_write))
150 self.json_pretty_print = True
151 self.add_settable(cmd2.Settable('json_pretty_print',
152 bool, 'Pretty-Print JSON output'))
153 self.apdu_trace = False
154 self.add_settable(cmd2.Settable('apdu_trace', bool, 'Trace and display APDUs exchanged with card',
155 onchange_cb=self._onchange_apdu_trace))
Philipp Maier5d698e52021-09-16 13:18:01 +0200156
Harald Weltec91085e2022-02-10 18:05:45 +0100157 self.equip(card, rs)
Philipp Maier5d698e52021-09-16 13:18:01 +0200158
Harald Weltec91085e2022-02-10 18:05:45 +0100159 def equip(self, card, rs):
160 """
161 Equip pySim-shell with the supplied card and runtime state, add (or remove) all required settables and
162 and commands to enable card operations.
163 """
Philipp Maier76667642021-09-22 16:53:22 +0200164
Harald Weltec91085e2022-02-10 18:05:45 +0100165 rc = False
Philipp Maier5d698e52021-09-16 13:18:01 +0200166
Harald Weltec91085e2022-02-10 18:05:45 +0100167 # Unequip everything from pySim-shell that would not work in unequipped state
168 if self.rs:
Harald Weltea6c0f882022-07-17 14:23:17 +0200169 lchan = self.rs.lchan[0]
170 lchan.unregister_cmds(self)
Harald Weltec91085e2022-02-10 18:05:45 +0100171 for cmds in [Iso7816Commands, PySimCommands]:
172 cmd_set = self.find_commandsets(cmds)
173 if cmd_set:
174 self.unregister_command_set(cmd_set[0])
Philipp Maier5d698e52021-09-16 13:18:01 +0200175
Harald Weltec91085e2022-02-10 18:05:45 +0100176 self.card = card
177 self.rs = rs
Philipp Maier5d698e52021-09-16 13:18:01 +0200178
Harald Weltec91085e2022-02-10 18:05:45 +0100179 # When a card object and a runtime state is present, (re)equip pySim-shell with everything that is
180 # needed to operate on cards.
181 if self.card and self.rs:
Harald Weltea6c0f882022-07-17 14:23:17 +0200182 self.lchan = self.rs.lchan[0]
Harald Weltec91085e2022-02-10 18:05:45 +0100183 self._onchange_conserve_write(
184 'conserve_write', False, self.conserve_write)
185 self._onchange_apdu_trace('apdu_trace', False, self.apdu_trace)
186 self.register_command_set(Iso7816Commands())
Harald Welte3c9b7842021-10-19 21:44:24 +0200187 self.register_command_set(Ts102222Commands())
Harald Weltec91085e2022-02-10 18:05:45 +0100188 self.register_command_set(PySimCommands())
189 self.iccid, sw = self.card.read_iccid()
Harald Weltea6c0f882022-07-17 14:23:17 +0200190 self.lchan.select('MF', self)
Harald Weltec91085e2022-02-10 18:05:45 +0100191 rc = True
192 else:
193 self.poutput("pySim-shell not equipped!")
Philipp Maier5d698e52021-09-16 13:18:01 +0200194
Harald Weltec91085e2022-02-10 18:05:45 +0100195 self.update_prompt()
196 return rc
Harald Welteb2edd142021-01-08 23:29:35 +0100197
Harald Weltec91085e2022-02-10 18:05:45 +0100198 def poutput_json(self, data, force_no_pretty=False):
199 """like cmd2.poutput() but for a JSON serializable dict."""
200 if force_no_pretty or self.json_pretty_print == False:
201 output = json.dumps(data, cls=JsonEncoder)
202 else:
203 output = json.dumps(data, cls=JsonEncoder, indent=4)
204 self.poutput(output)
Harald Welteb2edd142021-01-08 23:29:35 +0100205
Harald Weltec91085e2022-02-10 18:05:45 +0100206 def _onchange_numeric_path(self, param_name, old, new):
207 self.update_prompt()
Philipp Maier38c74f62021-03-17 17:19:52 +0100208
Harald Weltec91085e2022-02-10 18:05:45 +0100209 def _onchange_conserve_write(self, param_name, old, new):
210 if self.rs:
211 self.rs.conserve_write = new
Harald Welte7829d8a2021-04-10 11:28:53 +0200212
Harald Weltec91085e2022-02-10 18:05:45 +0100213 def _onchange_apdu_trace(self, param_name, old, new):
214 if self.card:
215 if new == True:
216 self.card._scc._tp.apdu_tracer = self.Cmd2ApduTracer(self)
217 else:
218 self.card._scc._tp.apdu_tracer = None
Harald Welte7829d8a2021-04-10 11:28:53 +0200219
Harald Weltec91085e2022-02-10 18:05:45 +0100220 class Cmd2ApduTracer(ApduTracer):
221 def __init__(self, cmd2_app):
222 self.cmd2 = app
Harald Welte7829d8a2021-04-10 11:28:53 +0200223
Harald Weltec91085e2022-02-10 18:05:45 +0100224 def trace_response(self, cmd, sw, resp):
225 self.cmd2.poutput("-> %s %s" % (cmd[:10], cmd[10:]))
226 self.cmd2.poutput("<- %s: %s" % (sw, resp))
Harald Welteb2edd142021-01-08 23:29:35 +0100227
Harald Weltec91085e2022-02-10 18:05:45 +0100228 def update_prompt(self):
Harald Weltea6c0f882022-07-17 14:23:17 +0200229 if self.lchan:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200230 path_str = self.lchan.selected_file.fully_qualified_path_str(not self.numeric_path)
231 self.prompt = 'pySIM-shell (%s)> ' % (path_str)
Harald Weltec91085e2022-02-10 18:05:45 +0100232 else:
Philipp Maier7226c092022-06-01 17:58:38 +0200233 if self.card:
234 self.prompt = 'pySIM-shell (no card profile)> '
235 else:
236 self.prompt = 'pySIM-shell (no card)> '
Harald Welteb2edd142021-01-08 23:29:35 +0100237
Harald Weltec91085e2022-02-10 18:05:45 +0100238 @cmd2.with_category(CUSTOM_CATEGORY)
239 def do_intro(self, _):
240 """Display the intro banner"""
241 self.poutput(self.intro)
Philipp Maier9764de22021-11-03 10:44:39 +0100242
Harald Weltec91085e2022-02-10 18:05:45 +0100243 def do_eof(self, _: argparse.Namespace) -> bool:
244 self.poutput("")
245 return self.do_quit('')
Philipp Maier5d698e52021-09-16 13:18:01 +0200246
Harald Weltec91085e2022-02-10 18:05:45 +0100247 @cmd2.with_category(CUSTOM_CATEGORY)
248 def do_equip(self, opts):
249 """Equip pySim-shell with card"""
250 rs, card = init_card(sl)
251 self.equip(card, rs)
Philipp Maier76667642021-09-22 16:53:22 +0200252
Philipp Maier7226c092022-06-01 17:58:38 +0200253 apdu_cmd_parser = argparse.ArgumentParser()
254 apdu_cmd_parser.add_argument('APDU', type=str, help='APDU as hex string')
Philipp Maiere7d1b672022-06-01 18:05:34 +0200255 apdu_cmd_parser.add_argument('--expect-sw', help='expect a specified status word', type=str, default=None)
Philipp Maier7226c092022-06-01 17:58:38 +0200256
257 @cmd2.with_argparser(apdu_cmd_parser)
258 def do_apdu(self, opts):
259 """Send a raw APDU to the card, and print SW + Response.
260 DANGEROUS: pySim-shell will not know any card state changes, and
261 not continue to work as expected if you e.g. select a different
262 file."""
263 data, sw = self.card._scc._tp.send_apdu(opts.APDU)
264 if data:
265 self.poutput("SW: %s, RESP: %s" % (sw, data))
266 else:
267 self.poutput("SW: %s" % sw)
Philipp Maiere7d1b672022-06-01 18:05:34 +0200268 if opts.expect_sw:
269 if not sw_match(sw, opts.expect_sw):
270 raise SwMatchError(sw, opts.expect_sw)
Philipp Maier7226c092022-06-01 17:58:38 +0200271
Harald Weltec91085e2022-02-10 18:05:45 +0100272 class InterceptStderr(list):
273 def __init__(self):
274 self._stderr_backup = sys.stderr
Philipp Maier76667642021-09-22 16:53:22 +0200275
Harald Weltec91085e2022-02-10 18:05:45 +0100276 def __enter__(self):
277 self._stringio_stderr = StringIO()
278 sys.stderr = self._stringio_stderr
279 return self
Philipp Maier76667642021-09-22 16:53:22 +0200280
Harald Weltec91085e2022-02-10 18:05:45 +0100281 def __exit__(self, *args):
282 self.stderr = self._stringio_stderr.getvalue().strip()
283 del self._stringio_stderr
284 sys.stderr = self._stderr_backup
Philipp Maier76667642021-09-22 16:53:22 +0200285
Harald Weltec91085e2022-02-10 18:05:45 +0100286 def _show_failure_sign(self):
287 self.poutput(style(" +-------------+", fg=fg.bright_red))
288 self.poutput(style(" + ## ## +", fg=fg.bright_red))
289 self.poutput(style(" + ## ## +", fg=fg.bright_red))
290 self.poutput(style(" + ### +", fg=fg.bright_red))
291 self.poutput(style(" + ## ## +", fg=fg.bright_red))
292 self.poutput(style(" + ## ## +", fg=fg.bright_red))
293 self.poutput(style(" +-------------+", fg=fg.bright_red))
294 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200295
Harald Weltec91085e2022-02-10 18:05:45 +0100296 def _show_success_sign(self):
297 self.poutput(style(" +-------------+", fg=fg.bright_green))
298 self.poutput(style(" + ## +", fg=fg.bright_green))
299 self.poutput(style(" + ## +", fg=fg.bright_green))
300 self.poutput(style(" + # ## +", fg=fg.bright_green))
301 self.poutput(style(" + ## # +", fg=fg.bright_green))
302 self.poutput(style(" + ## +", fg=fg.bright_green))
303 self.poutput(style(" +-------------+", fg=fg.bright_green))
304 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200305
Harald Weltec91085e2022-02-10 18:05:45 +0100306 def _process_card(self, first, script_path):
Philipp Maier76667642021-09-22 16:53:22 +0200307
Harald Weltec91085e2022-02-10 18:05:45 +0100308 # Early phase of card initialzation (this part may fail with an exception)
309 try:
310 rs, card = init_card(self.sl)
311 rc = self.equip(card, rs)
312 except:
313 self.poutput("")
314 self.poutput("Card initialization failed with an exception:")
315 self.poutput("---------------------8<---------------------")
316 traceback.print_exc()
317 self.poutput("---------------------8<---------------------")
318 self.poutput("")
319 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200320
Harald Weltec91085e2022-02-10 18:05:45 +0100321 # Actual card processing step. This part should never fail with an exception since the cmd2
322 # do_run_script method will catch any exception that might occur during script execution.
323 if rc:
324 self.poutput("")
325 self.poutput("Transcript stdout:")
326 self.poutput("---------------------8<---------------------")
327 with self.InterceptStderr() as logged:
328 self.do_run_script(script_path)
329 self.poutput("---------------------8<---------------------")
Philipp Maier76667642021-09-22 16:53:22 +0200330
Harald Weltec91085e2022-02-10 18:05:45 +0100331 self.poutput("")
332 self.poutput("Transcript stderr:")
333 if logged.stderr:
334 self.poutput("---------------------8<---------------------")
335 self.poutput(logged.stderr)
336 self.poutput("---------------------8<---------------------")
337 else:
338 self.poutput("(none)")
Philipp Maier76667642021-09-22 16:53:22 +0200339
Harald Weltec91085e2022-02-10 18:05:45 +0100340 # Check for exceptions
341 self.poutput("")
342 if "EXCEPTION of type" not in logged.stderr:
343 return 0
Philipp Maier76667642021-09-22 16:53:22 +0200344
Harald Weltec91085e2022-02-10 18:05:45 +0100345 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200346
Harald Weltec91085e2022-02-10 18:05:45 +0100347 bulk_script_parser = argparse.ArgumentParser()
348 bulk_script_parser.add_argument(
349 'script_path', help="path to the script file")
350 bulk_script_parser.add_argument('--halt_on_error', help='stop card handling if an exeption occurs',
351 action='store_true')
352 bulk_script_parser.add_argument('--tries', type=int, default=2,
353 help='how many tries before trying the next card')
354 bulk_script_parser.add_argument('--on_stop_action', type=str, default=None,
355 help='commandline to execute when card handling has stopped')
356 bulk_script_parser.add_argument('--pre_card_action', type=str, default=None,
357 help='commandline to execute before actually talking to the card')
Philipp Maier76667642021-09-22 16:53:22 +0200358
Harald Weltec91085e2022-02-10 18:05:45 +0100359 @cmd2.with_argparser(bulk_script_parser)
360 @cmd2.with_category(CUSTOM_CATEGORY)
361 def do_bulk_script(self, opts):
362 """Run script on multiple cards (bulk provisioning)"""
Philipp Maier76667642021-09-22 16:53:22 +0200363
Harald Weltec91085e2022-02-10 18:05:45 +0100364 # Make sure that the script file exists and that it is readable.
365 if not os.access(opts.script_path, os.R_OK):
366 self.poutput("Invalid script file!")
367 return
Philipp Maier76667642021-09-22 16:53:22 +0200368
Harald Weltec91085e2022-02-10 18:05:45 +0100369 success_count = 0
370 fail_count = 0
Philipp Maier76667642021-09-22 16:53:22 +0200371
Harald Weltec91085e2022-02-10 18:05:45 +0100372 first = True
373 while 1:
374 # TODO: Count consecutive failures, if more than N consecutive failures occur, then stop.
375 # The ratinale is: There may be a problem with the device, we do want to prevent that
376 # all remaining cards are fired to the error bin. This is only relevant for situations
377 # with large stacks, probably we do not need this feature right now.
Philipp Maier76667642021-09-22 16:53:22 +0200378
Harald Weltec91085e2022-02-10 18:05:45 +0100379 try:
380 # In case of failure, try multiple times.
381 for i in range(opts.tries):
382 # fetch card into reader bay
383 ch.get(first)
Philipp Maier76667642021-09-22 16:53:22 +0200384
Harald Weltec91085e2022-02-10 18:05:45 +0100385 # if necessary execute an action before we start processing the card
386 if(opts.pre_card_action):
387 os.system(opts.pre_card_action)
Philipp Maier76667642021-09-22 16:53:22 +0200388
Harald Weltec91085e2022-02-10 18:05:45 +0100389 # process the card
390 rc = self._process_card(first, opts.script_path)
391 if rc == 0:
392 success_count = success_count + 1
393 self._show_success_sign()
394 self.poutput("Statistics: success :%i, failure: %i" % (
395 success_count, fail_count))
396 break
397 else:
398 fail_count = fail_count + 1
399 self._show_failure_sign()
400 self.poutput("Statistics: success :%i, failure: %i" % (
401 success_count, fail_count))
Philipp Maier76667642021-09-22 16:53:22 +0200402
Harald Weltec91085e2022-02-10 18:05:45 +0100403 # Depending on success or failure, the card goes either in the "error" bin or in the
404 # "done" bin.
405 if rc < 0:
406 ch.error()
407 else:
408 ch.done()
Philipp Maier76667642021-09-22 16:53:22 +0200409
Harald Weltec91085e2022-02-10 18:05:45 +0100410 # In most cases it is possible to proceed with the next card, but the
411 # user may decide to halt immediately when an error occurs
412 if opts.halt_on_error and rc < 0:
413 return
Philipp Maier76667642021-09-22 16:53:22 +0200414
Harald Weltec91085e2022-02-10 18:05:45 +0100415 except (KeyboardInterrupt):
416 self.poutput("")
417 self.poutput("Terminated by user!")
418 return
419 except (SystemExit):
420 # When all cards are processed the card handler device will throw a SystemExit
421 # exception. Also Errors that are not recoverable (cards stuck etc.) will end up here.
422 # The user has the option to execute some action to make aware that the card handler
423 # needs service.
424 if(opts.on_stop_action):
425 os.system(opts.on_stop_action)
426 return
427 except:
428 self.poutput("")
429 self.poutput("Card handling failed with an exception:")
430 self.poutput("---------------------8<---------------------")
431 traceback.print_exc()
432 self.poutput("---------------------8<---------------------")
433 self.poutput("")
434 fail_count = fail_count + 1
435 self._show_failure_sign()
436 self.poutput("Statistics: success :%i, failure: %i" %
437 (success_count, fail_count))
Philipp Maierb52feed2021-09-22 16:43:13 +0200438
Harald Weltec91085e2022-02-10 18:05:45 +0100439 first = False
440
441 echo_parser = argparse.ArgumentParser()
442 echo_parser.add_argument('string', help="string to echo on the shell")
443
444 @cmd2.with_argparser(echo_parser)
445 @cmd2.with_category(CUSTOM_CATEGORY)
446 def do_echo(self, opts):
447 """Echo (print) a string on the console"""
448 self.poutput(opts.string)
449
Harald Weltefc315482022-07-23 12:49:14 +0200450 @cmd2.with_category(CUSTOM_CATEGORY)
451 def do_version(self, opts):
452 """Print the pySim software version."""
453 import pkg_resources
454 self.poutput(pkg_resources.get_distribution('pySim'))
Harald Welteb2edd142021-01-08 23:29:35 +0100455
Harald Welte31d2cf02021-04-03 10:47:29 +0200456@with_default_category('pySim Commands')
457class PySimCommands(CommandSet):
Harald Weltec91085e2022-02-10 18:05:45 +0100458 def __init__(self):
459 super().__init__()
Harald Welteb2edd142021-01-08 23:29:35 +0100460
Harald Weltec91085e2022-02-10 18:05:45 +0100461 dir_parser = argparse.ArgumentParser()
462 dir_parser.add_argument(
463 '--fids', help='Show file identifiers', action='store_true')
464 dir_parser.add_argument(
465 '--names', help='Show file names', action='store_true')
466 dir_parser.add_argument(
467 '--apps', help='Show applications', action='store_true')
468 dir_parser.add_argument(
469 '--all', help='Show all selectable identifiers and names', action='store_true')
Philipp Maier5d3e2592021-02-22 17:22:16 +0100470
Harald Weltec91085e2022-02-10 18:05:45 +0100471 @cmd2.with_argparser(dir_parser)
472 def do_dir(self, opts):
473 """Show a listing of files available in currently selected DF or MF"""
474 if opts.all:
475 flags = []
476 elif opts.fids or opts.names or opts.apps:
477 flags = ['PARENT', 'SELF']
478 if opts.fids:
479 flags += ['FIDS', 'AIDS']
480 if opts.names:
481 flags += ['FNAMES', 'ANAMES']
482 if opts.apps:
483 flags += ['ANAMES', 'AIDS']
484 else:
485 flags = ['PARENT', 'SELF', 'FNAMES', 'ANAMES']
486 selectables = list(
Harald Weltea6c0f882022-07-17 14:23:17 +0200487 self._cmd.lchan.selected_file.get_selectable_names(flags=flags))
Harald Weltec91085e2022-02-10 18:05:45 +0100488 directory_str = tabulate_str_list(
489 selectables, width=79, hspace=2, lspace=1, align_left=True)
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200490 path = self._cmd.lchan.selected_file.fully_qualified_path_str(True)
491 self._cmd.poutput(path)
492 path = self._cmd.lchan.selected_file.fully_qualified_path_str(False)
493 self._cmd.poutput(path)
Harald Weltec91085e2022-02-10 18:05:45 +0100494 self._cmd.poutput(directory_str)
495 self._cmd.poutput("%d files" % len(selectables))
Harald Welteb2edd142021-01-08 23:29:35 +0100496
Philipp Maier7b138b02022-05-31 13:42:56 +0200497 def walk(self, indent=0, action_ef=None, action_df=None, context=None, **kwargs):
Harald Weltec91085e2022-02-10 18:05:45 +0100498 """Recursively walk through the file system, starting at the currently selected DF"""
Philipp Maier7b138b02022-05-31 13:42:56 +0200499
Harald Weltea6c0f882022-07-17 14:23:17 +0200500 if isinstance(self._cmd.lchan.selected_file, CardDF):
Philipp Maier7b138b02022-05-31 13:42:56 +0200501 if action_df:
502 action_df(context, opts)
503
Harald Weltea6c0f882022-07-17 14:23:17 +0200504 files = self._cmd.lchan.selected_file.get_selectables(
Harald Weltec91085e2022-02-10 18:05:45 +0100505 flags=['FNAMES', 'ANAMES'])
506 for f in files:
Philipp Maier7b138b02022-05-31 13:42:56 +0200507 # special case: When no action is performed, just output a directory
508 if not action_ef and not action_df:
Harald Weltec91085e2022-02-10 18:05:45 +0100509 output_str = " " * indent + str(f) + (" " * 250)
510 output_str = output_str[0:25]
511 if isinstance(files[f], CardADF):
512 output_str += " " + str(files[f].aid)
513 else:
514 output_str += " " + str(files[f].fid)
515 output_str += " " + str(files[f].desc)
516 self._cmd.poutput(output_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200517
Harald Weltec91085e2022-02-10 18:05:45 +0100518 if isinstance(files[f], CardDF):
519 skip_df = False
520 try:
Harald Weltea6c0f882022-07-17 14:23:17 +0200521 fcp_dec = self._cmd.lchan.select(f, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100522 except Exception as e:
523 skip_df = True
Harald Weltea6c0f882022-07-17 14:23:17 +0200524 df = self._cmd.lchan.selected_file
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200525 df_path = df.fully_qualified_path_str(True)
526 df_skip_reason_str = df_path + \
Harald Weltec91085e2022-02-10 18:05:45 +0100527 "/" + str(f) + ", " + str(e)
528 if context:
529 context['DF_SKIP'] += 1
530 context['DF_SKIP_REASON'].append(df_skip_reason_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200531
Harald Weltec91085e2022-02-10 18:05:45 +0100532 # If the DF was skipped, we never have entered the directory
533 # below, so we must not move up.
534 if skip_df == False:
Philipp Maier7b138b02022-05-31 13:42:56 +0200535 self.walk(indent + 1, action_ef, action_df, context, **kwargs)
Harald Weltea6c0f882022-07-17 14:23:17 +0200536 fcp_dec = self._cmd.lchan.select("..", self._cmd)
Philipp Maierf408a402021-04-09 21:16:12 +0200537
Philipp Maier7b138b02022-05-31 13:42:56 +0200538 elif action_ef:
Harald Weltea6c0f882022-07-17 14:23:17 +0200539 df_before_action = self._cmd.lchan.selected_file
Philipp Maier7b138b02022-05-31 13:42:56 +0200540 action_ef(f, context, **kwargs)
Harald Weltec91085e2022-02-10 18:05:45 +0100541 # When walking through the file system tree the action must not
542 # always restore the currently selected file to the file that
543 # was selected before executing the action() callback.
Harald Weltea6c0f882022-07-17 14:23:17 +0200544 if df_before_action != self._cmd.lchan.selected_file:
Harald Weltec91085e2022-02-10 18:05:45 +0100545 raise RuntimeError("inconsistent walk, %s is currently selected but expecting %s to be selected"
Harald Weltea6c0f882022-07-17 14:23:17 +0200546 % (str(self._cmd.lchan.selected_file), str(df_before_action)))
Philipp Maierff9dae22021-02-25 17:03:21 +0100547
Harald Weltec91085e2022-02-10 18:05:45 +0100548 def do_tree(self, opts):
549 """Display a filesystem-tree with all selectable files"""
550 self.walk()
Philipp Maierff9dae22021-02-25 17:03:21 +0100551
Philipp Maier7b138b02022-05-31 13:42:56 +0200552 def export_ef(self, filename, context, as_json):
553 """ Select and export a single elementary file (EF) """
Harald Weltec91085e2022-02-10 18:05:45 +0100554 context['COUNT'] += 1
Harald Weltea6c0f882022-07-17 14:23:17 +0200555 df = self._cmd.lchan.selected_file
Philipp Maierac34dcc2021-04-01 17:19:05 +0200556
Philipp Maierea81f752022-05-19 10:13:30 +0200557 # The currently selected file (not the file we are going to export)
558 # must always be an ADF or DF. From this starting point we select
559 # the EF we want to export. To maintain consistency we will then
560 # select the current DF again (see comment below).
Harald Weltec91085e2022-02-10 18:05:45 +0100561 if not isinstance(df, CardDF):
562 raise RuntimeError(
563 "currently selected file %s is not a DF or ADF" % str(df))
Philipp Maierac34dcc2021-04-01 17:19:05 +0200564
Harald Weltec91085e2022-02-10 18:05:45 +0100565 df_path_list = df.fully_qualified_path(True)
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200566 df_path = df.fully_qualified_path_str(True)
567 df_path_fid = df.fully_qualified_path_str(False)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100568
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200569 file_str = df_path + "/" + str(filename)
Harald Weltec91085e2022-02-10 18:05:45 +0100570 self._cmd.poutput(boxed_heading_str(file_str))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100571
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200572 self._cmd.poutput("# directory: %s (%s)" % (df_path, df_path_fid))
Harald Weltec91085e2022-02-10 18:05:45 +0100573 try:
Harald Weltea6c0f882022-07-17 14:23:17 +0200574 fcp_dec = self._cmd.lchan.select(filename, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100575 self._cmd.poutput("# file: %s (%s)" % (
Harald Weltea6c0f882022-07-17 14:23:17 +0200576 self._cmd.lchan.selected_file.name, self._cmd.lchan.selected_file.fid))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100577
Harald Weltea6c0f882022-07-17 14:23:17 +0200578 structure = self._cmd.lchan.selected_file_structure()
Harald Weltec91085e2022-02-10 18:05:45 +0100579 self._cmd.poutput("# structure: %s" % str(structure))
Harald Weltea6c0f882022-07-17 14:23:17 +0200580 self._cmd.poutput("# RAW FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp_hex))
581 self._cmd.poutput("# Decoded FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100582
Harald Weltec91085e2022-02-10 18:05:45 +0100583 for f in df_path_list:
584 self._cmd.poutput("select " + str(f))
Harald Weltea6c0f882022-07-17 14:23:17 +0200585 self._cmd.poutput("select " + self._cmd.lchan.selected_file.name)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100586
Harald Weltec91085e2022-02-10 18:05:45 +0100587 if structure == 'transparent':
Harald Welte08b11ab2022-02-10 18:56:41 +0100588 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200589 result = self._cmd.lchan.read_binary_dec()
Harald Welte08b11ab2022-02-10 18:56:41 +0100590 self._cmd.poutput("update_binary_decoded '%s'" % json.dumps(result[0], cls=JsonEncoder))
591 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200592 result = self._cmd.lchan.read_binary()
Harald Welte08b11ab2022-02-10 18:56:41 +0100593 self._cmd.poutput("update_binary " + str(result[0]))
Harald Weltec91085e2022-02-10 18:05:45 +0100594 elif structure == 'cyclic' or structure == 'linear_fixed':
595 # Use number of records specified in select response
Harald Weltea6c0f882022-07-17 14:23:17 +0200596 num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
Harald Welte747a9782022-02-13 17:52:28 +0100597 if num_of_rec:
Harald Weltec91085e2022-02-10 18:05:45 +0100598 for r in range(1, num_of_rec + 1):
Harald Welte08b11ab2022-02-10 18:56:41 +0100599 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200600 result = self._cmd.lchan.read_record_dec(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100601 self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
602 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200603 result = self._cmd.lchan.read_record(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100604 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
605
Harald Weltec91085e2022-02-10 18:05:45 +0100606 # When the select response does not return the number of records, read until we hit the
607 # first record that cannot be read.
608 else:
609 r = 1
610 while True:
611 try:
Harald Welte08b11ab2022-02-10 18:56:41 +0100612 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200613 result = self._cmd.lchan.read_record_dec(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100614 self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
615 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200616 result = self._cmd.lchan.read_record(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100617 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
Harald Weltec91085e2022-02-10 18:05:45 +0100618 except SwMatchError as e:
619 # We are past the last valid record - stop
620 if e.sw_actual == "9402":
621 break
622 # Some other problem occurred
623 else:
624 raise e
Harald Weltec91085e2022-02-10 18:05:45 +0100625 r = r + 1
626 elif structure == 'ber_tlv':
Harald Weltea6c0f882022-07-17 14:23:17 +0200627 tags = self._cmd.lchan.retrieve_tags()
Harald Weltec91085e2022-02-10 18:05:45 +0100628 for t in tags:
Harald Weltea6c0f882022-07-17 14:23:17 +0200629 result = self._cmd.lchan.retrieve_data(t)
Harald Weltec91085e2022-02-10 18:05:45 +0100630 (tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
631 self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val)))
632 else:
633 raise RuntimeError(
634 'Unsupported structure "%s" of file "%s"' % (structure, filename))
635 except Exception as e:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200636 bad_file_str = df_path + "/" + str(filename) + ", " + str(e)
Harald Weltec91085e2022-02-10 18:05:45 +0100637 self._cmd.poutput("# bad file: %s" % bad_file_str)
638 context['ERR'] += 1
639 context['BAD'].append(bad_file_str)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100640
Harald Weltec91085e2022-02-10 18:05:45 +0100641 # When reading the file is done, make sure the parent file is
642 # selected again. This will be the usual case, however we need
643 # to check before since we must not select the same DF twice
Harald Weltea6c0f882022-07-17 14:23:17 +0200644 if df != self._cmd.lchan.selected_file:
645 self._cmd.lchan.select(df.fid or df.aid, self._cmd)
Philipp Maierac34dcc2021-04-01 17:19:05 +0200646
Harald Weltec91085e2022-02-10 18:05:45 +0100647 self._cmd.poutput("#")
Philipp Maier24f7bd32021-02-25 17:06:18 +0100648
Harald Weltec91085e2022-02-10 18:05:45 +0100649 export_parser = argparse.ArgumentParser()
650 export_parser.add_argument(
651 '--filename', type=str, default=None, help='only export specific file')
Harald Welte08b11ab2022-02-10 18:56:41 +0100652 export_parser.add_argument(
653 '--json', action='store_true', help='export as JSON (less reliable)')
Philipp Maier24f7bd32021-02-25 17:06:18 +0100654
Harald Weltec91085e2022-02-10 18:05:45 +0100655 @cmd2.with_argparser(export_parser)
656 def do_export(self, opts):
657 """Export files to script that can be imported back later"""
658 context = {'ERR': 0, 'COUNT': 0, 'BAD': [],
659 'DF_SKIP': 0, 'DF_SKIP_REASON': []}
Philipp Maier9a4091d2022-05-19 10:20:30 +0200660 kwargs_export = {'as_json': opts.json}
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200661 exception_str_add = ""
662
Harald Weltec91085e2022-02-10 18:05:45 +0100663 if opts.filename:
Philipp Maier7b138b02022-05-31 13:42:56 +0200664 self.export_ef(opts.filename, context, **kwargs_export)
Harald Weltec91085e2022-02-10 18:05:45 +0100665 else:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200666 try:
667 self.walk(0, self.export_ef, None, context, **kwargs_export)
668 except Exception as e:
669 print("# Stopping early here due to exception: " + str(e))
670 print("#")
671 exception_str_add = ", also had to stop early due to exception:" + str(e)
Philipp Maier80ce71f2021-04-19 21:24:23 +0200672
Harald Weltec91085e2022-02-10 18:05:45 +0100673 self._cmd.poutput(boxed_heading_str("Export summary"))
Philipp Maier80ce71f2021-04-19 21:24:23 +0200674
Harald Weltec91085e2022-02-10 18:05:45 +0100675 self._cmd.poutput("# total files visited: %u" % context['COUNT'])
676 self._cmd.poutput("# bad files: %u" % context['ERR'])
677 for b in context['BAD']:
678 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200679
Harald Weltec91085e2022-02-10 18:05:45 +0100680 self._cmd.poutput("# skipped dedicated files(s): %u" %
681 context['DF_SKIP'])
682 for b in context['DF_SKIP_REASON']:
683 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200684
Harald Weltec91085e2022-02-10 18:05:45 +0100685 if context['ERR'] and context['DF_SKIP']:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200686 raise RuntimeError("unable to export %i elementary file(s) and %i dedicated file(s)%s" % (
687 context['ERR'], context['DF_SKIP'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100688 elif context['ERR']:
689 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200690 "unable to export %i elementary file(s)%s" % (context['ERR'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100691 elif context['DF_SKIP']:
692 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200693 "unable to export %i dedicated files(s)%s" % (context['ERR'], exception_str_add))
Harald Welteb2edd142021-01-08 23:29:35 +0100694
Harald Weltec91085e2022-02-10 18:05:45 +0100695 def do_reset(self, opts):
696 """Reset the Card."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200697 atr = self._cmd.lchan.reset(self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100698 self._cmd.poutput('Card ATR: %s' % atr)
699 self._cmd.update_prompt()
Harald Weltedaf2b392021-05-03 23:17:29 +0200700
Harald Weltec91085e2022-02-10 18:05:45 +0100701 def do_desc(self, opts):
702 """Display human readable file description for the currently selected file"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200703 desc = self._cmd.lchan.selected_file.desc
Harald Weltec91085e2022-02-10 18:05:45 +0100704 if desc:
705 self._cmd.poutput(desc)
706 else:
707 self._cmd.poutput("no description available")
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200708
Harald Weltec91085e2022-02-10 18:05:45 +0100709 def do_verify_adm(self, arg):
710 """VERIFY the ADM1 PIN"""
711 if arg:
712 # use specified ADM-PIN
713 pin_adm = sanitize_pin_adm(arg)
714 else:
715 # try to find an ADM-PIN if none is specified
716 result = card_key_provider_get_field(
717 'ADM1', key='ICCID', value=self._cmd.iccid)
718 pin_adm = sanitize_pin_adm(result)
719 if pin_adm:
720 self._cmd.poutput(
721 "found ADM-PIN '%s' for ICCID '%s'" % (result, self._cmd.iccid))
722 else:
723 raise ValueError(
724 "cannot find ADM-PIN for ICCID '%s'" % (self._cmd.iccid))
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200725
Harald Weltec91085e2022-02-10 18:05:45 +0100726 if pin_adm:
727 self._cmd.card.verify_adm(h2b(pin_adm))
728 else:
729 raise ValueError("error: cannot authenticate, no adm-pin!")
730
Harald Welteb2edd142021-01-08 23:29:35 +0100731
Harald Welte31d2cf02021-04-03 10:47:29 +0200732@with_default_category('ISO7816 Commands')
733class Iso7816Commands(CommandSet):
Harald Weltec91085e2022-02-10 18:05:45 +0100734 def __init__(self):
735 super().__init__()
Harald Welte31d2cf02021-04-03 10:47:29 +0200736
Harald Weltec91085e2022-02-10 18:05:45 +0100737 def do_select(self, opts):
738 """SELECT a File (ADF/DF/EF)"""
739 if len(opts.arg_list) == 0:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200740 path = self._cmd.lchan.selected_file.fully_qualified_path_str(True)
741 path_fid = self._cmd.lchan.selected_file.fully_qualified_path_str(False)
742 self._cmd.poutput("currently selected file: %s (%s)" % (path, path_fid))
Harald Weltec91085e2022-02-10 18:05:45 +0100743 return
Harald Welte31d2cf02021-04-03 10:47:29 +0200744
Harald Weltec91085e2022-02-10 18:05:45 +0100745 path = opts.arg_list[0]
Harald Weltea6c0f882022-07-17 14:23:17 +0200746 fcp_dec = self._cmd.lchan.select(path, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100747 self._cmd.update_prompt()
748 self._cmd.poutput_json(fcp_dec)
Harald Welte31d2cf02021-04-03 10:47:29 +0200749
Harald Weltec91085e2022-02-10 18:05:45 +0100750 def complete_select(self, text, line, begidx, endidx) -> List[str]:
751 """Command Line tab completion for SELECT"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200752 index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
Harald Weltec91085e2022-02-10 18:05:45 +0100753 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200754
Harald Weltec91085e2022-02-10 18:05:45 +0100755 def get_code(self, code):
756 """Use code either directly or try to get it from external data source"""
757 auto = ('PIN1', 'PIN2', 'PUK1', 'PUK2')
Harald Welte31d2cf02021-04-03 10:47:29 +0200758
Harald Weltec91085e2022-02-10 18:05:45 +0100759 if str(code).upper() not in auto:
760 return sanitize_pin_adm(code)
Harald Welte31d2cf02021-04-03 10:47:29 +0200761
Harald Weltec91085e2022-02-10 18:05:45 +0100762 result = card_key_provider_get_field(
763 str(code), key='ICCID', value=self._cmd.iccid)
764 result = sanitize_pin_adm(result)
765 if result:
766 self._cmd.poutput("found %s '%s' for ICCID '%s'" %
767 (code.upper(), result, self._cmd.iccid))
768 else:
769 self._cmd.poutput("cannot find %s for ICCID '%s'" %
770 (code.upper(), self._cmd.iccid))
771 return result
Harald Welte31d2cf02021-04-03 10:47:29 +0200772
Harald Weltec91085e2022-02-10 18:05:45 +0100773 verify_chv_parser = argparse.ArgumentParser()
774 verify_chv_parser.add_argument(
775 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
776 verify_chv_parser.add_argument(
777 '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 +0200778
Harald Weltec91085e2022-02-10 18:05:45 +0100779 @cmd2.with_argparser(verify_chv_parser)
780 def do_verify_chv(self, opts):
Harald Welte12af7932022-02-15 16:39:08 +0100781 """Verify (authenticate) using specified CHV (PIN) code, which is how the specifications
782 call it if you authenticate yourself using the specified PIN. There usually is at least PIN1 and
783 PIN2."""
Harald Weltec91085e2022-02-10 18:05:45 +0100784 pin = self.get_code(opts.pin_code)
785 (data, sw) = self._cmd.card._scc.verify_chv(opts.pin_nr, h2b(pin))
786 self._cmd.poutput("CHV verification successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200787
Harald Weltec91085e2022-02-10 18:05:45 +0100788 unblock_chv_parser = argparse.ArgumentParser()
789 unblock_chv_parser.add_argument(
790 '--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
791 unblock_chv_parser.add_argument(
792 'puk_code', type=str, help='PUK code digits \"PUK1\" or \"PUK2\" to get PUK code from external data source')
793 unblock_chv_parser.add_argument(
794 '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 +0200795
Harald Weltec91085e2022-02-10 18:05:45 +0100796 @cmd2.with_argparser(unblock_chv_parser)
797 def do_unblock_chv(self, opts):
798 """Unblock PIN code using specified PUK code"""
799 new_pin = self.get_code(opts.new_pin_code)
800 puk = self.get_code(opts.puk_code)
801 (data, sw) = self._cmd.card._scc.unblock_chv(
802 opts.pin_nr, h2b(puk), h2b(new_pin))
803 self._cmd.poutput("CHV unblock successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200804
Harald Weltec91085e2022-02-10 18:05:45 +0100805 change_chv_parser = argparse.ArgumentParser()
806 change_chv_parser.add_argument(
807 '--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
808 change_chv_parser.add_argument(
809 'pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
810 change_chv_parser.add_argument(
811 '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 +0200812
Harald Weltec91085e2022-02-10 18:05:45 +0100813 @cmd2.with_argparser(change_chv_parser)
814 def do_change_chv(self, opts):
815 """Change PIN code to a new PIN code"""
816 new_pin = self.get_code(opts.new_pin_code)
817 pin = self.get_code(opts.pin_code)
818 (data, sw) = self._cmd.card._scc.change_chv(
819 opts.pin_nr, h2b(pin), h2b(new_pin))
820 self._cmd.poutput("CHV change successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200821
Harald Weltec91085e2022-02-10 18:05:45 +0100822 disable_chv_parser = argparse.ArgumentParser()
823 disable_chv_parser.add_argument(
824 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
825 disable_chv_parser.add_argument(
826 '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 +0200827
Harald Weltec91085e2022-02-10 18:05:45 +0100828 @cmd2.with_argparser(disable_chv_parser)
829 def do_disable_chv(self, opts):
830 """Disable PIN code using specified PIN code"""
831 pin = self.get_code(opts.pin_code)
832 (data, sw) = self._cmd.card._scc.disable_chv(opts.pin_nr, h2b(pin))
833 self._cmd.poutput("CHV disable successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200834
Harald Weltec91085e2022-02-10 18:05:45 +0100835 enable_chv_parser = argparse.ArgumentParser()
836 enable_chv_parser.add_argument(
837 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
838 enable_chv_parser.add_argument(
839 '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 +0200840
Harald Weltec91085e2022-02-10 18:05:45 +0100841 @cmd2.with_argparser(enable_chv_parser)
842 def do_enable_chv(self, opts):
843 """Enable PIN code using specified PIN code"""
844 pin = self.get_code(opts.pin_code)
845 (data, sw) = self._cmd.card._scc.enable_chv(opts.pin_nr, h2b(pin))
846 self._cmd.poutput("CHV enable successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200847
Harald Weltec91085e2022-02-10 18:05:45 +0100848 def do_deactivate_file(self, opts):
Harald Welte799c3542022-02-15 15:56:28 +0100849 """Deactivate the currently selected EF"""
Harald Weltec91085e2022-02-10 18:05:45 +0100850 (data, sw) = self._cmd.card._scc.deactivate_file()
Harald Weltea4631612021-04-10 18:17:55 +0200851
Harald Welte799c3542022-02-15 15:56:28 +0100852 activate_file_parser = argparse.ArgumentParser()
853 activate_file_parser.add_argument('NAME', type=str, help='File name or FID of file to activate')
854 @cmd2.with_argparser(activate_file_parser)
Harald Weltec91085e2022-02-10 18:05:45 +0100855 def do_activate_file(self, opts):
Harald Welte12af7932022-02-15 16:39:08 +0100856 """Activate the specified EF. This used to be called REHABILITATE in TS 11.11 for classic
857 SIM. You need to specify the name or FID of the file to activate."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200858 (data, sw) = self._cmd.lchan.activate_file(opts.NAME)
Harald Welte485692b2021-05-25 22:21:44 +0200859
Harald Weltec91085e2022-02-10 18:05:45 +0100860 def complete_activate_file(self, text, line, begidx, endidx) -> List[str]:
861 """Command Line tab completion for ACTIVATE FILE"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200862 index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
Harald Weltec91085e2022-02-10 18:05:45 +0100863 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200864
Harald Weltec91085e2022-02-10 18:05:45 +0100865 open_chan_parser = argparse.ArgumentParser()
866 open_chan_parser.add_argument(
867 'chan_nr', type=int, default=0, help='Channel Number')
Harald Welte703f9332021-04-10 18:39:32 +0200868
Harald Weltec91085e2022-02-10 18:05:45 +0100869 @cmd2.with_argparser(open_chan_parser)
870 def do_open_channel(self, opts):
871 """Open a logical channel."""
872 (data, sw) = self._cmd.card._scc.manage_channel(
873 mode='open', lchan_nr=opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200874
Harald Weltec91085e2022-02-10 18:05:45 +0100875 close_chan_parser = argparse.ArgumentParser()
876 close_chan_parser.add_argument(
877 'chan_nr', type=int, default=0, help='Channel Number')
Harald Welte703f9332021-04-10 18:39:32 +0200878
Harald Weltec91085e2022-02-10 18:05:45 +0100879 @cmd2.with_argparser(close_chan_parser)
880 def do_close_channel(self, opts):
881 """Close a logical channel."""
882 (data, sw) = self._cmd.card._scc.manage_channel(
883 mode='close', lchan_nr=opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200884
Harald Weltec91085e2022-02-10 18:05:45 +0100885 def do_status(self, opts):
886 """Perform the STATUS command."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200887 fcp_dec = self._cmd.lchan.status()
Harald Weltec91085e2022-02-10 18:05:45 +0100888 self._cmd.poutput_json(fcp_dec)
Harald Welte34b05d32021-05-25 22:03:13 +0200889
Harald Weltec91085e2022-02-10 18:05:45 +0100890 suspend_uicc_parser = argparse.ArgumentParser()
891 suspend_uicc_parser.add_argument('--min-duration-secs', type=int, default=60,
892 help='Proposed minimum duration of suspension')
893 suspend_uicc_parser.add_argument('--max-duration-secs', type=int, default=24*60*60,
894 help='Proposed maximum duration of suspension')
Harald Welteec950532021-10-20 13:09:00 +0200895
Harald Weltec91085e2022-02-10 18:05:45 +0100896 # not ISO7816-4 but TS 102 221
897 @cmd2.with_argparser(suspend_uicc_parser)
898 def do_suspend_uicc(self, opts):
899 """Perform the SUSPEND UICC command. Only supported on some UICC."""
900 (duration, token, sw) = self._cmd.card._scc.suspend_uicc(min_len_secs=opts.min_duration_secs,
901 max_len_secs=opts.max_duration_secs)
902 self._cmd.poutput(
903 'Negotiated Duration: %u secs, Token: %s, SW: %s' % (duration, token, sw))
Harald Welteec950532021-10-20 13:09:00 +0200904
Harald Welte703f9332021-04-10 18:39:32 +0200905
Harald Weltef2e761c2021-04-11 11:56:44 +0200906option_parser = argparse.ArgumentParser(prog='pySim-shell', description='interactive SIM card shell',
907 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
Harald Welte28c24312021-04-11 12:19:36 +0200908argparse_add_reader_args(option_parser)
Harald Weltec8ff0262021-04-11 12:06:13 +0200909
910global_group = option_parser.add_argument_group('General Options')
911global_group.add_argument('--script', metavar='PATH', default=None,
Harald Welte28c24312021-04-11 12:19:36 +0200912 help='script with pySim-shell commands to be executed automatically at start-up')
Harald Weltec91085e2022-02-10 18:05:45 +0100913global_group.add_argument('--csv', metavar='FILE',
914 default=None, help='Read card data from CSV file')
Philipp Maier76667642021-09-22 16:53:22 +0200915global_group.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
Harald Weltec91085e2022-02-10 18:05:45 +0100916 help="Use automatic card handling machine")
Harald Weltec8ff0262021-04-11 12:06:13 +0200917
918adm_group = global_group.add_mutually_exclusive_group()
919adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
920 help='ADM PIN used for provisioning (overwrites default)')
921adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM1_HEX', dest='pin_adm_hex', default=None,
922 help='ADM PIN used for provisioning, as hex string (16 characters long)')
Harald Welteb2edd142021-01-08 23:29:35 +0100923
924
925if __name__ == '__main__':
926
Harald Weltec91085e2022-02-10 18:05:45 +0100927 # Parse options
928 opts = option_parser.parse_args()
Harald Welteb2edd142021-01-08 23:29:35 +0100929
Harald Weltec91085e2022-02-10 18:05:45 +0100930 # If a script file is specified, be sure that it actually exists
931 if opts.script:
932 if not os.access(opts.script, os.R_OK):
933 print("Invalid script file!")
934 sys.exit(2)
Philipp Maier13e258d2021-04-08 17:48:49 +0200935
Harald Weltec91085e2022-02-10 18:05:45 +0100936 # Register csv-file as card data provider, either from specified CSV
937 # or from CSV file in home directory
938 csv_default = str(Path.home()) + "/.osmocom/pysim/card_data.csv"
939 if opts.csv:
940 card_key_provider_register(CardKeyProviderCsv(opts.csv))
941 if os.path.isfile(csv_default):
942 card_key_provider_register(CardKeyProviderCsv(csv_default))
Philipp Maier2b11c322021-03-17 12:37:39 +0100943
Harald Weltec91085e2022-02-10 18:05:45 +0100944 # Init card reader driver
945 sl = init_reader(opts)
946 if sl is None:
947 exit(1)
Philipp Maierea95c392021-09-16 13:10:19 +0200948
Harald Weltec91085e2022-02-10 18:05:45 +0100949 # Create command layer
950 scc = SimCardCommands(transport=sl)
Philipp Maierea95c392021-09-16 13:10:19 +0200951
Harald Weltec91085e2022-02-10 18:05:45 +0100952 # Create a card handler (for bulk provisioning)
953 if opts.card_handler_config:
954 ch = CardHandlerAuto(None, opts.card_handler_config)
955 else:
956 ch = CardHandler(sl)
Philipp Maier76667642021-09-22 16:53:22 +0200957
Harald Weltec91085e2022-02-10 18:05:45 +0100958 # Detect and initialize the card in the reader. This may fail when there
959 # is no card in the reader or the card is unresponsive. PysimApp is
960 # able to tolerate and recover from that.
961 try:
962 rs, card = init_card(sl)
963 app = PysimApp(card, rs, sl, ch, opts.script)
964 except:
965 print("Card initialization failed with an exception:")
966 print("---------------------8<---------------------")
967 traceback.print_exc()
968 print("---------------------8<---------------------")
969 print("(you may still try to recover from this manually by using the 'equip' command.)")
970 print(
971 " it should also be noted that some readers may behave strangely when no card")
972 print(" is inserted.)")
973 print("")
Philipp Maier7226c092022-06-01 17:58:38 +0200974 app = PysimApp(card, None, sl, ch, opts.script)
Philipp Maierea95c392021-09-16 13:10:19 +0200975
Harald Weltec91085e2022-02-10 18:05:45 +0100976 # If the user supplies an ADM PIN at via commandline args authenticate
977 # immediately so that the user does not have to use the shell commands
978 pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
979 if pin_adm:
980 if not card:
981 print("Card error, cannot do ADM verification with supplied ADM pin now.")
982 try:
983 card.verify_adm(h2b(pin_adm))
984 except Exception as e:
985 print(e)
Philipp Maier228c98e2021-03-10 20:14:06 +0100986
Harald Weltec91085e2022-02-10 18:05:45 +0100987 app.cmdloop()