blob: c05fd7a92dfba477fa52682c9cb9eba8b845c821 [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:
230 path_list = self.lchan.selected_file.fully_qualified_path(
Harald Weltec91085e2022-02-10 18:05:45 +0100231 not self.numeric_path)
232 self.prompt = 'pySIM-shell (%s)> ' % ('/'.join(path_list))
233 else:
Philipp Maier7226c092022-06-01 17:58:38 +0200234 if self.card:
235 self.prompt = 'pySIM-shell (no card profile)> '
236 else:
237 self.prompt = 'pySIM-shell (no card)> '
Harald Welteb2edd142021-01-08 23:29:35 +0100238
Harald Weltec91085e2022-02-10 18:05:45 +0100239 @cmd2.with_category(CUSTOM_CATEGORY)
240 def do_intro(self, _):
241 """Display the intro banner"""
242 self.poutput(self.intro)
Philipp Maier9764de22021-11-03 10:44:39 +0100243
Harald Weltec91085e2022-02-10 18:05:45 +0100244 def do_eof(self, _: argparse.Namespace) -> bool:
245 self.poutput("")
246 return self.do_quit('')
Philipp Maier5d698e52021-09-16 13:18:01 +0200247
Harald Weltec91085e2022-02-10 18:05:45 +0100248 @cmd2.with_category(CUSTOM_CATEGORY)
249 def do_equip(self, opts):
250 """Equip pySim-shell with card"""
251 rs, card = init_card(sl)
252 self.equip(card, rs)
Philipp Maier76667642021-09-22 16:53:22 +0200253
Philipp Maier7226c092022-06-01 17:58:38 +0200254 apdu_cmd_parser = argparse.ArgumentParser()
255 apdu_cmd_parser.add_argument('APDU', type=str, help='APDU as hex string')
Philipp Maiere7d1b672022-06-01 18:05:34 +0200256 apdu_cmd_parser.add_argument('--expect-sw', help='expect a specified status word', type=str, default=None)
Philipp Maier7226c092022-06-01 17:58:38 +0200257
258 @cmd2.with_argparser(apdu_cmd_parser)
259 def do_apdu(self, opts):
260 """Send a raw APDU to the card, and print SW + Response.
261 DANGEROUS: pySim-shell will not know any card state changes, and
262 not continue to work as expected if you e.g. select a different
263 file."""
264 data, sw = self.card._scc._tp.send_apdu(opts.APDU)
265 if data:
266 self.poutput("SW: %s, RESP: %s" % (sw, data))
267 else:
268 self.poutput("SW: %s" % sw)
Philipp Maiere7d1b672022-06-01 18:05:34 +0200269 if opts.expect_sw:
270 if not sw_match(sw, opts.expect_sw):
271 raise SwMatchError(sw, opts.expect_sw)
Philipp Maier7226c092022-06-01 17:58:38 +0200272
Harald Weltec91085e2022-02-10 18:05:45 +0100273 class InterceptStderr(list):
274 def __init__(self):
275 self._stderr_backup = sys.stderr
Philipp Maier76667642021-09-22 16:53:22 +0200276
Harald Weltec91085e2022-02-10 18:05:45 +0100277 def __enter__(self):
278 self._stringio_stderr = StringIO()
279 sys.stderr = self._stringio_stderr
280 return self
Philipp Maier76667642021-09-22 16:53:22 +0200281
Harald Weltec91085e2022-02-10 18:05:45 +0100282 def __exit__(self, *args):
283 self.stderr = self._stringio_stderr.getvalue().strip()
284 del self._stringio_stderr
285 sys.stderr = self._stderr_backup
Philipp Maier76667642021-09-22 16:53:22 +0200286
Harald Weltec91085e2022-02-10 18:05:45 +0100287 def _show_failure_sign(self):
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(style(" +-------------+", fg=fg.bright_red))
295 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200296
Harald Weltec91085e2022-02-10 18:05:45 +0100297 def _show_success_sign(self):
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(style(" +-------------+", fg=fg.bright_green))
305 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200306
Harald Weltec91085e2022-02-10 18:05:45 +0100307 def _process_card(self, first, script_path):
Philipp Maier76667642021-09-22 16:53:22 +0200308
Harald Weltec91085e2022-02-10 18:05:45 +0100309 # Early phase of card initialzation (this part may fail with an exception)
310 try:
311 rs, card = init_card(self.sl)
312 rc = self.equip(card, rs)
313 except:
314 self.poutput("")
315 self.poutput("Card initialization failed with an exception:")
316 self.poutput("---------------------8<---------------------")
317 traceback.print_exc()
318 self.poutput("---------------------8<---------------------")
319 self.poutput("")
320 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200321
Harald Weltec91085e2022-02-10 18:05:45 +0100322 # Actual card processing step. This part should never fail with an exception since the cmd2
323 # do_run_script method will catch any exception that might occur during script execution.
324 if rc:
325 self.poutput("")
326 self.poutput("Transcript stdout:")
327 self.poutput("---------------------8<---------------------")
328 with self.InterceptStderr() as logged:
329 self.do_run_script(script_path)
330 self.poutput("---------------------8<---------------------")
Philipp Maier76667642021-09-22 16:53:22 +0200331
Harald Weltec91085e2022-02-10 18:05:45 +0100332 self.poutput("")
333 self.poutput("Transcript stderr:")
334 if logged.stderr:
335 self.poutput("---------------------8<---------------------")
336 self.poutput(logged.stderr)
337 self.poutput("---------------------8<---------------------")
338 else:
339 self.poutput("(none)")
Philipp Maier76667642021-09-22 16:53:22 +0200340
Harald Weltec91085e2022-02-10 18:05:45 +0100341 # Check for exceptions
342 self.poutput("")
343 if "EXCEPTION of type" not in logged.stderr:
344 return 0
Philipp Maier76667642021-09-22 16:53:22 +0200345
Harald Weltec91085e2022-02-10 18:05:45 +0100346 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200347
Harald Weltec91085e2022-02-10 18:05:45 +0100348 bulk_script_parser = argparse.ArgumentParser()
349 bulk_script_parser.add_argument(
350 'script_path', help="path to the script file")
351 bulk_script_parser.add_argument('--halt_on_error', help='stop card handling if an exeption occurs',
352 action='store_true')
353 bulk_script_parser.add_argument('--tries', type=int, default=2,
354 help='how many tries before trying the next card')
355 bulk_script_parser.add_argument('--on_stop_action', type=str, default=None,
356 help='commandline to execute when card handling has stopped')
357 bulk_script_parser.add_argument('--pre_card_action', type=str, default=None,
358 help='commandline to execute before actually talking to the card')
Philipp Maier76667642021-09-22 16:53:22 +0200359
Harald Weltec91085e2022-02-10 18:05:45 +0100360 @cmd2.with_argparser(bulk_script_parser)
361 @cmd2.with_category(CUSTOM_CATEGORY)
362 def do_bulk_script(self, opts):
363 """Run script on multiple cards (bulk provisioning)"""
Philipp Maier76667642021-09-22 16:53:22 +0200364
Harald Weltec91085e2022-02-10 18:05:45 +0100365 # Make sure that the script file exists and that it is readable.
366 if not os.access(opts.script_path, os.R_OK):
367 self.poutput("Invalid script file!")
368 return
Philipp Maier76667642021-09-22 16:53:22 +0200369
Harald Weltec91085e2022-02-10 18:05:45 +0100370 success_count = 0
371 fail_count = 0
Philipp Maier76667642021-09-22 16:53:22 +0200372
Harald Weltec91085e2022-02-10 18:05:45 +0100373 first = True
374 while 1:
375 # TODO: Count consecutive failures, if more than N consecutive failures occur, then stop.
376 # The ratinale is: There may be a problem with the device, we do want to prevent that
377 # all remaining cards are fired to the error bin. This is only relevant for situations
378 # with large stacks, probably we do not need this feature right now.
Philipp Maier76667642021-09-22 16:53:22 +0200379
Harald Weltec91085e2022-02-10 18:05:45 +0100380 try:
381 # In case of failure, try multiple times.
382 for i in range(opts.tries):
383 # fetch card into reader bay
384 ch.get(first)
Philipp Maier76667642021-09-22 16:53:22 +0200385
Harald Weltec91085e2022-02-10 18:05:45 +0100386 # if necessary execute an action before we start processing the card
387 if(opts.pre_card_action):
388 os.system(opts.pre_card_action)
Philipp Maier76667642021-09-22 16:53:22 +0200389
Harald Weltec91085e2022-02-10 18:05:45 +0100390 # process the card
391 rc = self._process_card(first, opts.script_path)
392 if rc == 0:
393 success_count = success_count + 1
394 self._show_success_sign()
395 self.poutput("Statistics: success :%i, failure: %i" % (
396 success_count, fail_count))
397 break
398 else:
399 fail_count = fail_count + 1
400 self._show_failure_sign()
401 self.poutput("Statistics: success :%i, failure: %i" % (
402 success_count, fail_count))
Philipp Maier76667642021-09-22 16:53:22 +0200403
Harald Weltec91085e2022-02-10 18:05:45 +0100404 # Depending on success or failure, the card goes either in the "error" bin or in the
405 # "done" bin.
406 if rc < 0:
407 ch.error()
408 else:
409 ch.done()
Philipp Maier76667642021-09-22 16:53:22 +0200410
Harald Weltec91085e2022-02-10 18:05:45 +0100411 # In most cases it is possible to proceed with the next card, but the
412 # user may decide to halt immediately when an error occurs
413 if opts.halt_on_error and rc < 0:
414 return
Philipp Maier76667642021-09-22 16:53:22 +0200415
Harald Weltec91085e2022-02-10 18:05:45 +0100416 except (KeyboardInterrupt):
417 self.poutput("")
418 self.poutput("Terminated by user!")
419 return
420 except (SystemExit):
421 # When all cards are processed the card handler device will throw a SystemExit
422 # exception. Also Errors that are not recoverable (cards stuck etc.) will end up here.
423 # The user has the option to execute some action to make aware that the card handler
424 # needs service.
425 if(opts.on_stop_action):
426 os.system(opts.on_stop_action)
427 return
428 except:
429 self.poutput("")
430 self.poutput("Card handling failed with an exception:")
431 self.poutput("---------------------8<---------------------")
432 traceback.print_exc()
433 self.poutput("---------------------8<---------------------")
434 self.poutput("")
435 fail_count = fail_count + 1
436 self._show_failure_sign()
437 self.poutput("Statistics: success :%i, failure: %i" %
438 (success_count, fail_count))
Philipp Maierb52feed2021-09-22 16:43:13 +0200439
Harald Weltec91085e2022-02-10 18:05:45 +0100440 first = False
441
442 echo_parser = argparse.ArgumentParser()
443 echo_parser.add_argument('string', help="string to echo on the shell")
444
445 @cmd2.with_argparser(echo_parser)
446 @cmd2.with_category(CUSTOM_CATEGORY)
447 def do_echo(self, opts):
448 """Echo (print) a string on the console"""
449 self.poutput(opts.string)
450
Harald Welteb2edd142021-01-08 23:29:35 +0100451
Harald Welte31d2cf02021-04-03 10:47:29 +0200452@with_default_category('pySim Commands')
453class PySimCommands(CommandSet):
Harald Weltec91085e2022-02-10 18:05:45 +0100454 def __init__(self):
455 super().__init__()
Harald Welteb2edd142021-01-08 23:29:35 +0100456
Harald Weltec91085e2022-02-10 18:05:45 +0100457 dir_parser = argparse.ArgumentParser()
458 dir_parser.add_argument(
459 '--fids', help='Show file identifiers', action='store_true')
460 dir_parser.add_argument(
461 '--names', help='Show file names', action='store_true')
462 dir_parser.add_argument(
463 '--apps', help='Show applications', action='store_true')
464 dir_parser.add_argument(
465 '--all', help='Show all selectable identifiers and names', action='store_true')
Philipp Maier5d3e2592021-02-22 17:22:16 +0100466
Harald Weltec91085e2022-02-10 18:05:45 +0100467 @cmd2.with_argparser(dir_parser)
468 def do_dir(self, opts):
469 """Show a listing of files available in currently selected DF or MF"""
470 if opts.all:
471 flags = []
472 elif opts.fids or opts.names or opts.apps:
473 flags = ['PARENT', 'SELF']
474 if opts.fids:
475 flags += ['FIDS', 'AIDS']
476 if opts.names:
477 flags += ['FNAMES', 'ANAMES']
478 if opts.apps:
479 flags += ['ANAMES', 'AIDS']
480 else:
481 flags = ['PARENT', 'SELF', 'FNAMES', 'ANAMES']
482 selectables = list(
Harald Weltea6c0f882022-07-17 14:23:17 +0200483 self._cmd.lchan.selected_file.get_selectable_names(flags=flags))
Harald Weltec91085e2022-02-10 18:05:45 +0100484 directory_str = tabulate_str_list(
485 selectables, width=79, hspace=2, lspace=1, align_left=True)
Harald Weltea6c0f882022-07-17 14:23:17 +0200486 path_list = self._cmd.lchan.selected_file.fully_qualified_path(True)
Harald Weltec91085e2022-02-10 18:05:45 +0100487 self._cmd.poutput('/'.join(path_list))
Harald Weltea6c0f882022-07-17 14:23:17 +0200488 path_list = self._cmd.lchan.selected_file.fully_qualified_path(False)
Harald Weltec91085e2022-02-10 18:05:45 +0100489 self._cmd.poutput('/'.join(path_list))
490 self._cmd.poutput(directory_str)
491 self._cmd.poutput("%d files" % len(selectables))
Harald Welteb2edd142021-01-08 23:29:35 +0100492
Philipp Maier7b138b02022-05-31 13:42:56 +0200493 def walk(self, indent=0, action_ef=None, action_df=None, context=None, **kwargs):
Harald Weltec91085e2022-02-10 18:05:45 +0100494 """Recursively walk through the file system, starting at the currently selected DF"""
Philipp Maier7b138b02022-05-31 13:42:56 +0200495
Harald Weltea6c0f882022-07-17 14:23:17 +0200496 if isinstance(self._cmd.lchan.selected_file, CardDF):
Philipp Maier7b138b02022-05-31 13:42:56 +0200497 if action_df:
498 action_df(context, opts)
499
Harald Weltea6c0f882022-07-17 14:23:17 +0200500 files = self._cmd.lchan.selected_file.get_selectables(
Harald Weltec91085e2022-02-10 18:05:45 +0100501 flags=['FNAMES', 'ANAMES'])
502 for f in files:
Philipp Maier7b138b02022-05-31 13:42:56 +0200503 # special case: When no action is performed, just output a directory
504 if not action_ef and not action_df:
Harald Weltec91085e2022-02-10 18:05:45 +0100505 output_str = " " * indent + str(f) + (" " * 250)
506 output_str = output_str[0:25]
507 if isinstance(files[f], CardADF):
508 output_str += " " + str(files[f].aid)
509 else:
510 output_str += " " + str(files[f].fid)
511 output_str += " " + str(files[f].desc)
512 self._cmd.poutput(output_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200513
Harald Weltec91085e2022-02-10 18:05:45 +0100514 if isinstance(files[f], CardDF):
515 skip_df = False
516 try:
Harald Weltea6c0f882022-07-17 14:23:17 +0200517 fcp_dec = self._cmd.lchan.select(f, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100518 except Exception as e:
519 skip_df = True
Harald Weltea6c0f882022-07-17 14:23:17 +0200520 df = self._cmd.lchan.selected_file
Harald Weltec91085e2022-02-10 18:05:45 +0100521 df_path_list = df.fully_qualified_path(True)
522 df_skip_reason_str = '/'.join(df_path_list) + \
523 "/" + str(f) + ", " + str(e)
524 if context:
525 context['DF_SKIP'] += 1
526 context['DF_SKIP_REASON'].append(df_skip_reason_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200527
Harald Weltec91085e2022-02-10 18:05:45 +0100528 # If the DF was skipped, we never have entered the directory
529 # below, so we must not move up.
530 if skip_df == False:
Philipp Maier7b138b02022-05-31 13:42:56 +0200531 self.walk(indent + 1, action_ef, action_df, context, **kwargs)
Harald Weltea6c0f882022-07-17 14:23:17 +0200532 fcp_dec = self._cmd.lchan.select("..", self._cmd)
Philipp Maierf408a402021-04-09 21:16:12 +0200533
Philipp Maier7b138b02022-05-31 13:42:56 +0200534 elif action_ef:
Harald Weltea6c0f882022-07-17 14:23:17 +0200535 df_before_action = self._cmd.lchan.selected_file
Philipp Maier7b138b02022-05-31 13:42:56 +0200536 action_ef(f, context, **kwargs)
Harald Weltec91085e2022-02-10 18:05:45 +0100537 # When walking through the file system tree the action must not
538 # always restore the currently selected file to the file that
539 # was selected before executing the action() callback.
Harald Weltea6c0f882022-07-17 14:23:17 +0200540 if df_before_action != self._cmd.lchan.selected_file:
Harald Weltec91085e2022-02-10 18:05:45 +0100541 raise RuntimeError("inconsistent walk, %s is currently selected but expecting %s to be selected"
Harald Weltea6c0f882022-07-17 14:23:17 +0200542 % (str(self._cmd.lchan.selected_file), str(df_before_action)))
Philipp Maierff9dae22021-02-25 17:03:21 +0100543
Harald Weltec91085e2022-02-10 18:05:45 +0100544 def do_tree(self, opts):
545 """Display a filesystem-tree with all selectable files"""
546 self.walk()
Philipp Maierff9dae22021-02-25 17:03:21 +0100547
Philipp Maier7b138b02022-05-31 13:42:56 +0200548 def export_ef(self, filename, context, as_json):
549 """ Select and export a single elementary file (EF) """
Harald Weltec91085e2022-02-10 18:05:45 +0100550 context['COUNT'] += 1
Harald Weltea6c0f882022-07-17 14:23:17 +0200551 df = self._cmd.lchan.selected_file
Philipp Maierac34dcc2021-04-01 17:19:05 +0200552
Philipp Maierea81f752022-05-19 10:13:30 +0200553 # The currently selected file (not the file we are going to export)
554 # must always be an ADF or DF. From this starting point we select
555 # the EF we want to export. To maintain consistency we will then
556 # select the current DF again (see comment below).
Harald Weltec91085e2022-02-10 18:05:45 +0100557 if not isinstance(df, CardDF):
558 raise RuntimeError(
559 "currently selected file %s is not a DF or ADF" % str(df))
Philipp Maierac34dcc2021-04-01 17:19:05 +0200560
Harald Weltec91085e2022-02-10 18:05:45 +0100561 df_path_list = df.fully_qualified_path(True)
562 df_path_list_fid = df.fully_qualified_path(False)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100563
Harald Weltec91085e2022-02-10 18:05:45 +0100564 file_str = '/'.join(df_path_list) + "/" + str(filename)
565 self._cmd.poutput(boxed_heading_str(file_str))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100566
Harald Weltec91085e2022-02-10 18:05:45 +0100567 self._cmd.poutput("# directory: %s (%s)" %
568 ('/'.join(df_path_list), '/'.join(df_path_list_fid)))
569 try:
Harald Weltea6c0f882022-07-17 14:23:17 +0200570 fcp_dec = self._cmd.lchan.select(filename, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100571 self._cmd.poutput("# file: %s (%s)" % (
Harald Weltea6c0f882022-07-17 14:23:17 +0200572 self._cmd.lchan.selected_file.name, self._cmd.lchan.selected_file.fid))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100573
Harald Weltea6c0f882022-07-17 14:23:17 +0200574 structure = self._cmd.lchan.selected_file_structure()
Harald Weltec91085e2022-02-10 18:05:45 +0100575 self._cmd.poutput("# structure: %s" % str(structure))
Harald Weltea6c0f882022-07-17 14:23:17 +0200576 self._cmd.poutput("# RAW FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp_hex))
577 self._cmd.poutput("# Decoded FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100578
Harald Weltec91085e2022-02-10 18:05:45 +0100579 for f in df_path_list:
580 self._cmd.poutput("select " + str(f))
Harald Weltea6c0f882022-07-17 14:23:17 +0200581 self._cmd.poutput("select " + self._cmd.lchan.selected_file.name)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100582
Harald Weltec91085e2022-02-10 18:05:45 +0100583 if structure == 'transparent':
Harald Welte08b11ab2022-02-10 18:56:41 +0100584 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200585 result = self._cmd.lchan.read_binary_dec()
Harald Welte08b11ab2022-02-10 18:56:41 +0100586 self._cmd.poutput("update_binary_decoded '%s'" % json.dumps(result[0], cls=JsonEncoder))
587 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200588 result = self._cmd.lchan.read_binary()
Harald Welte08b11ab2022-02-10 18:56:41 +0100589 self._cmd.poutput("update_binary " + str(result[0]))
Harald Weltec91085e2022-02-10 18:05:45 +0100590 elif structure == 'cyclic' or structure == 'linear_fixed':
591 # Use number of records specified in select response
Harald Weltea6c0f882022-07-17 14:23:17 +0200592 num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
Harald Welte747a9782022-02-13 17:52:28 +0100593 if num_of_rec:
Harald Weltec91085e2022-02-10 18:05:45 +0100594 for r in range(1, num_of_rec + 1):
Harald Welte08b11ab2022-02-10 18:56:41 +0100595 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200596 result = self._cmd.lchan.read_record_dec(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100597 self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
598 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200599 result = self._cmd.lchan.read_record(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100600 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
601
Harald Weltec91085e2022-02-10 18:05:45 +0100602 # When the select response does not return the number of records, read until we hit the
603 # first record that cannot be read.
604 else:
605 r = 1
606 while True:
607 try:
Harald Welte08b11ab2022-02-10 18:56:41 +0100608 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200609 result = self._cmd.lchan.read_record_dec(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100610 self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
611 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200612 result = self._cmd.lchan.read_record(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100613 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
Harald Weltec91085e2022-02-10 18:05:45 +0100614 except SwMatchError as e:
615 # We are past the last valid record - stop
616 if e.sw_actual == "9402":
617 break
618 # Some other problem occurred
619 else:
620 raise e
Harald Weltec91085e2022-02-10 18:05:45 +0100621 r = r + 1
622 elif structure == 'ber_tlv':
Harald Weltea6c0f882022-07-17 14:23:17 +0200623 tags = self._cmd.lchan.retrieve_tags()
Harald Weltec91085e2022-02-10 18:05:45 +0100624 for t in tags:
Harald Weltea6c0f882022-07-17 14:23:17 +0200625 result = self._cmd.lchan.retrieve_data(t)
Harald Weltec91085e2022-02-10 18:05:45 +0100626 (tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
627 self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val)))
628 else:
629 raise RuntimeError(
630 'Unsupported structure "%s" of file "%s"' % (structure, filename))
631 except Exception as e:
632 bad_file_str = '/'.join(df_path_list) + \
633 "/" + str(filename) + ", " + str(e)
634 self._cmd.poutput("# bad file: %s" % bad_file_str)
635 context['ERR'] += 1
636 context['BAD'].append(bad_file_str)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100637
Harald Weltec91085e2022-02-10 18:05:45 +0100638 # When reading the file is done, make sure the parent file is
639 # selected again. This will be the usual case, however we need
640 # to check before since we must not select the same DF twice
Harald Weltea6c0f882022-07-17 14:23:17 +0200641 if df != self._cmd.lchan.selected_file:
642 self._cmd.lchan.select(df.fid or df.aid, self._cmd)
Philipp Maierac34dcc2021-04-01 17:19:05 +0200643
Harald Weltec91085e2022-02-10 18:05:45 +0100644 self._cmd.poutput("#")
Philipp Maier24f7bd32021-02-25 17:06:18 +0100645
Harald Weltec91085e2022-02-10 18:05:45 +0100646 export_parser = argparse.ArgumentParser()
647 export_parser.add_argument(
648 '--filename', type=str, default=None, help='only export specific file')
Harald Welte08b11ab2022-02-10 18:56:41 +0100649 export_parser.add_argument(
650 '--json', action='store_true', help='export as JSON (less reliable)')
Philipp Maier24f7bd32021-02-25 17:06:18 +0100651
Harald Weltec91085e2022-02-10 18:05:45 +0100652 @cmd2.with_argparser(export_parser)
653 def do_export(self, opts):
654 """Export files to script that can be imported back later"""
655 context = {'ERR': 0, 'COUNT': 0, 'BAD': [],
656 'DF_SKIP': 0, 'DF_SKIP_REASON': []}
Philipp Maier9a4091d2022-05-19 10:20:30 +0200657 kwargs_export = {'as_json': opts.json}
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200658 exception_str_add = ""
659
Harald Weltec91085e2022-02-10 18:05:45 +0100660 if opts.filename:
Philipp Maier7b138b02022-05-31 13:42:56 +0200661 self.export_ef(opts.filename, context, **kwargs_export)
Harald Weltec91085e2022-02-10 18:05:45 +0100662 else:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200663 try:
664 self.walk(0, self.export_ef, None, context, **kwargs_export)
665 except Exception as e:
666 print("# Stopping early here due to exception: " + str(e))
667 print("#")
668 exception_str_add = ", also had to stop early due to exception:" + str(e)
Philipp Maier80ce71f2021-04-19 21:24:23 +0200669
Harald Weltec91085e2022-02-10 18:05:45 +0100670 self._cmd.poutput(boxed_heading_str("Export summary"))
Philipp Maier80ce71f2021-04-19 21:24:23 +0200671
Harald Weltec91085e2022-02-10 18:05:45 +0100672 self._cmd.poutput("# total files visited: %u" % context['COUNT'])
673 self._cmd.poutput("# bad files: %u" % context['ERR'])
674 for b in context['BAD']:
675 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200676
Harald Weltec91085e2022-02-10 18:05:45 +0100677 self._cmd.poutput("# skipped dedicated files(s): %u" %
678 context['DF_SKIP'])
679 for b in context['DF_SKIP_REASON']:
680 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200681
Harald Weltec91085e2022-02-10 18:05:45 +0100682 if context['ERR'] and context['DF_SKIP']:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200683 raise RuntimeError("unable to export %i elementary file(s) and %i dedicated file(s)%s" % (
684 context['ERR'], context['DF_SKIP'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100685 elif context['ERR']:
686 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200687 "unable to export %i elementary file(s)%s" % (context['ERR'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100688 elif context['DF_SKIP']:
689 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200690 "unable to export %i dedicated files(s)%s" % (context['ERR'], exception_str_add))
Harald Welteb2edd142021-01-08 23:29:35 +0100691
Harald Weltec91085e2022-02-10 18:05:45 +0100692 def do_reset(self, opts):
693 """Reset the Card."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200694 atr = self._cmd.lchan.reset(self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100695 self._cmd.poutput('Card ATR: %s' % atr)
696 self._cmd.update_prompt()
Harald Weltedaf2b392021-05-03 23:17:29 +0200697
Harald Weltec91085e2022-02-10 18:05:45 +0100698 def do_desc(self, opts):
699 """Display human readable file description for the currently selected file"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200700 desc = self._cmd.lchan.selected_file.desc
Harald Weltec91085e2022-02-10 18:05:45 +0100701 if desc:
702 self._cmd.poutput(desc)
703 else:
704 self._cmd.poutput("no description available")
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200705
Harald Weltec91085e2022-02-10 18:05:45 +0100706 def do_verify_adm(self, arg):
707 """VERIFY the ADM1 PIN"""
708 if arg:
709 # use specified ADM-PIN
710 pin_adm = sanitize_pin_adm(arg)
711 else:
712 # try to find an ADM-PIN if none is specified
713 result = card_key_provider_get_field(
714 'ADM1', key='ICCID', value=self._cmd.iccid)
715 pin_adm = sanitize_pin_adm(result)
716 if pin_adm:
717 self._cmd.poutput(
718 "found ADM-PIN '%s' for ICCID '%s'" % (result, self._cmd.iccid))
719 else:
720 raise ValueError(
721 "cannot find ADM-PIN for ICCID '%s'" % (self._cmd.iccid))
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200722
Harald Weltec91085e2022-02-10 18:05:45 +0100723 if pin_adm:
724 self._cmd.card.verify_adm(h2b(pin_adm))
725 else:
726 raise ValueError("error: cannot authenticate, no adm-pin!")
727
Harald Welteb2edd142021-01-08 23:29:35 +0100728
Harald Welte31d2cf02021-04-03 10:47:29 +0200729@with_default_category('ISO7816 Commands')
730class Iso7816Commands(CommandSet):
Harald Weltec91085e2022-02-10 18:05:45 +0100731 def __init__(self):
732 super().__init__()
Harald Welte31d2cf02021-04-03 10:47:29 +0200733
Harald Weltec91085e2022-02-10 18:05:45 +0100734 def do_select(self, opts):
735 """SELECT a File (ADF/DF/EF)"""
736 if len(opts.arg_list) == 0:
Harald Weltea6c0f882022-07-17 14:23:17 +0200737 path_list = self._cmd.lchan.selected_file.fully_qualified_path(True)
738 path_list_fid = self._cmd.lchan.selected_file.fully_qualified_path(
Harald Weltec91085e2022-02-10 18:05:45 +0100739 False)
740 self._cmd.poutput("currently selected file: " +
741 '/'.join(path_list) + " (" + '/'.join(path_list_fid) + ")")
742 return
Harald Welte31d2cf02021-04-03 10:47:29 +0200743
Harald Weltec91085e2022-02-10 18:05:45 +0100744 path = opts.arg_list[0]
Harald Weltea6c0f882022-07-17 14:23:17 +0200745 fcp_dec = self._cmd.lchan.select(path, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100746 self._cmd.update_prompt()
747 self._cmd.poutput_json(fcp_dec)
Harald Welte31d2cf02021-04-03 10:47:29 +0200748
Harald Weltec91085e2022-02-10 18:05:45 +0100749 def complete_select(self, text, line, begidx, endidx) -> List[str]:
750 """Command Line tab completion for SELECT"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200751 index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
Harald Weltec91085e2022-02-10 18:05:45 +0100752 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200753
Harald Weltec91085e2022-02-10 18:05:45 +0100754 def get_code(self, code):
755 """Use code either directly or try to get it from external data source"""
756 auto = ('PIN1', 'PIN2', 'PUK1', 'PUK2')
Harald Welte31d2cf02021-04-03 10:47:29 +0200757
Harald Weltec91085e2022-02-10 18:05:45 +0100758 if str(code).upper() not in auto:
759 return sanitize_pin_adm(code)
Harald Welte31d2cf02021-04-03 10:47:29 +0200760
Harald Weltec91085e2022-02-10 18:05:45 +0100761 result = card_key_provider_get_field(
762 str(code), key='ICCID', value=self._cmd.iccid)
763 result = sanitize_pin_adm(result)
764 if result:
765 self._cmd.poutput("found %s '%s' for ICCID '%s'" %
766 (code.upper(), result, self._cmd.iccid))
767 else:
768 self._cmd.poutput("cannot find %s for ICCID '%s'" %
769 (code.upper(), self._cmd.iccid))
770 return result
Harald Welte31d2cf02021-04-03 10:47:29 +0200771
Harald Weltec91085e2022-02-10 18:05:45 +0100772 verify_chv_parser = argparse.ArgumentParser()
773 verify_chv_parser.add_argument(
774 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
775 verify_chv_parser.add_argument(
776 '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 +0200777
Harald Weltec91085e2022-02-10 18:05:45 +0100778 @cmd2.with_argparser(verify_chv_parser)
779 def do_verify_chv(self, opts):
Harald Welte12af7932022-02-15 16:39:08 +0100780 """Verify (authenticate) using specified CHV (PIN) code, which is how the specifications
781 call it if you authenticate yourself using the specified PIN. There usually is at least PIN1 and
782 PIN2."""
Harald Weltec91085e2022-02-10 18:05:45 +0100783 pin = self.get_code(opts.pin_code)
784 (data, sw) = self._cmd.card._scc.verify_chv(opts.pin_nr, h2b(pin))
785 self._cmd.poutput("CHV verification successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200786
Harald Weltec91085e2022-02-10 18:05:45 +0100787 unblock_chv_parser = argparse.ArgumentParser()
788 unblock_chv_parser.add_argument(
789 '--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
790 unblock_chv_parser.add_argument(
791 'puk_code', type=str, help='PUK code digits \"PUK1\" or \"PUK2\" to get PUK code from external data source')
792 unblock_chv_parser.add_argument(
793 '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 +0200794
Harald Weltec91085e2022-02-10 18:05:45 +0100795 @cmd2.with_argparser(unblock_chv_parser)
796 def do_unblock_chv(self, opts):
797 """Unblock PIN code using specified PUK code"""
798 new_pin = self.get_code(opts.new_pin_code)
799 puk = self.get_code(opts.puk_code)
800 (data, sw) = self._cmd.card._scc.unblock_chv(
801 opts.pin_nr, h2b(puk), h2b(new_pin))
802 self._cmd.poutput("CHV unblock successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200803
Harald Weltec91085e2022-02-10 18:05:45 +0100804 change_chv_parser = argparse.ArgumentParser()
805 change_chv_parser.add_argument(
806 '--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
807 change_chv_parser.add_argument(
808 'pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
809 change_chv_parser.add_argument(
810 '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 +0200811
Harald Weltec91085e2022-02-10 18:05:45 +0100812 @cmd2.with_argparser(change_chv_parser)
813 def do_change_chv(self, opts):
814 """Change PIN code to a new PIN code"""
815 new_pin = self.get_code(opts.new_pin_code)
816 pin = self.get_code(opts.pin_code)
817 (data, sw) = self._cmd.card._scc.change_chv(
818 opts.pin_nr, h2b(pin), h2b(new_pin))
819 self._cmd.poutput("CHV change successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200820
Harald Weltec91085e2022-02-10 18:05:45 +0100821 disable_chv_parser = argparse.ArgumentParser()
822 disable_chv_parser.add_argument(
823 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
824 disable_chv_parser.add_argument(
825 '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 +0200826
Harald Weltec91085e2022-02-10 18:05:45 +0100827 @cmd2.with_argparser(disable_chv_parser)
828 def do_disable_chv(self, opts):
829 """Disable PIN code using specified PIN code"""
830 pin = self.get_code(opts.pin_code)
831 (data, sw) = self._cmd.card._scc.disable_chv(opts.pin_nr, h2b(pin))
832 self._cmd.poutput("CHV disable successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200833
Harald Weltec91085e2022-02-10 18:05:45 +0100834 enable_chv_parser = argparse.ArgumentParser()
835 enable_chv_parser.add_argument(
836 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
837 enable_chv_parser.add_argument(
838 '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 +0200839
Harald Weltec91085e2022-02-10 18:05:45 +0100840 @cmd2.with_argparser(enable_chv_parser)
841 def do_enable_chv(self, opts):
842 """Enable PIN code using specified PIN code"""
843 pin = self.get_code(opts.pin_code)
844 (data, sw) = self._cmd.card._scc.enable_chv(opts.pin_nr, h2b(pin))
845 self._cmd.poutput("CHV enable successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200846
Harald Weltec91085e2022-02-10 18:05:45 +0100847 def do_deactivate_file(self, opts):
Harald Welte799c3542022-02-15 15:56:28 +0100848 """Deactivate the currently selected EF"""
Harald Weltec91085e2022-02-10 18:05:45 +0100849 (data, sw) = self._cmd.card._scc.deactivate_file()
Harald Weltea4631612021-04-10 18:17:55 +0200850
Harald Welte799c3542022-02-15 15:56:28 +0100851 activate_file_parser = argparse.ArgumentParser()
852 activate_file_parser.add_argument('NAME', type=str, help='File name or FID of file to activate')
853 @cmd2.with_argparser(activate_file_parser)
Harald Weltec91085e2022-02-10 18:05:45 +0100854 def do_activate_file(self, opts):
Harald Welte12af7932022-02-15 16:39:08 +0100855 """Activate the specified EF. This used to be called REHABILITATE in TS 11.11 for classic
856 SIM. You need to specify the name or FID of the file to activate."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200857 (data, sw) = self._cmd.lchan.activate_file(opts.NAME)
Harald Welte485692b2021-05-25 22:21:44 +0200858
Harald Weltec91085e2022-02-10 18:05:45 +0100859 def complete_activate_file(self, text, line, begidx, endidx) -> List[str]:
860 """Command Line tab completion for ACTIVATE FILE"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200861 index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
Harald Weltec91085e2022-02-10 18:05:45 +0100862 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200863
Harald Weltec91085e2022-02-10 18:05:45 +0100864 open_chan_parser = argparse.ArgumentParser()
865 open_chan_parser.add_argument(
866 'chan_nr', type=int, default=0, help='Channel Number')
Harald Welte703f9332021-04-10 18:39:32 +0200867
Harald Weltec91085e2022-02-10 18:05:45 +0100868 @cmd2.with_argparser(open_chan_parser)
869 def do_open_channel(self, opts):
870 """Open a logical channel."""
871 (data, sw) = self._cmd.card._scc.manage_channel(
872 mode='open', lchan_nr=opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200873
Harald Weltec91085e2022-02-10 18:05:45 +0100874 close_chan_parser = argparse.ArgumentParser()
875 close_chan_parser.add_argument(
876 'chan_nr', type=int, default=0, help='Channel Number')
Harald Welte703f9332021-04-10 18:39:32 +0200877
Harald Weltec91085e2022-02-10 18:05:45 +0100878 @cmd2.with_argparser(close_chan_parser)
879 def do_close_channel(self, opts):
880 """Close a logical channel."""
881 (data, sw) = self._cmd.card._scc.manage_channel(
882 mode='close', lchan_nr=opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200883
Harald Weltec91085e2022-02-10 18:05:45 +0100884 def do_status(self, opts):
885 """Perform the STATUS command."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200886 fcp_dec = self._cmd.lchan.status()
Harald Weltec91085e2022-02-10 18:05:45 +0100887 self._cmd.poutput_json(fcp_dec)
Harald Welte34b05d32021-05-25 22:03:13 +0200888
Harald Weltec91085e2022-02-10 18:05:45 +0100889 suspend_uicc_parser = argparse.ArgumentParser()
890 suspend_uicc_parser.add_argument('--min-duration-secs', type=int, default=60,
891 help='Proposed minimum duration of suspension')
892 suspend_uicc_parser.add_argument('--max-duration-secs', type=int, default=24*60*60,
893 help='Proposed maximum duration of suspension')
Harald Welteec950532021-10-20 13:09:00 +0200894
Harald Weltec91085e2022-02-10 18:05:45 +0100895 # not ISO7816-4 but TS 102 221
896 @cmd2.with_argparser(suspend_uicc_parser)
897 def do_suspend_uicc(self, opts):
898 """Perform the SUSPEND UICC command. Only supported on some UICC."""
899 (duration, token, sw) = self._cmd.card._scc.suspend_uicc(min_len_secs=opts.min_duration_secs,
900 max_len_secs=opts.max_duration_secs)
901 self._cmd.poutput(
902 'Negotiated Duration: %u secs, Token: %s, SW: %s' % (duration, token, sw))
Harald Welteec950532021-10-20 13:09:00 +0200903
Harald Welte703f9332021-04-10 18:39:32 +0200904
Harald Weltef2e761c2021-04-11 11:56:44 +0200905option_parser = argparse.ArgumentParser(prog='pySim-shell', description='interactive SIM card shell',
906 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
Harald Welte28c24312021-04-11 12:19:36 +0200907argparse_add_reader_args(option_parser)
Harald Weltec8ff0262021-04-11 12:06:13 +0200908
909global_group = option_parser.add_argument_group('General Options')
910global_group.add_argument('--script', metavar='PATH', default=None,
Harald Welte28c24312021-04-11 12:19:36 +0200911 help='script with pySim-shell commands to be executed automatically at start-up')
Harald Weltec91085e2022-02-10 18:05:45 +0100912global_group.add_argument('--csv', metavar='FILE',
913 default=None, help='Read card data from CSV file')
Philipp Maier76667642021-09-22 16:53:22 +0200914global_group.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
Harald Weltec91085e2022-02-10 18:05:45 +0100915 help="Use automatic card handling machine")
Harald Weltec8ff0262021-04-11 12:06:13 +0200916
917adm_group = global_group.add_mutually_exclusive_group()
918adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
919 help='ADM PIN used for provisioning (overwrites default)')
920adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM1_HEX', dest='pin_adm_hex', default=None,
921 help='ADM PIN used for provisioning, as hex string (16 characters long)')
Harald Welteb2edd142021-01-08 23:29:35 +0100922
923
924if __name__ == '__main__':
925
Harald Weltec91085e2022-02-10 18:05:45 +0100926 # Parse options
927 opts = option_parser.parse_args()
Harald Welteb2edd142021-01-08 23:29:35 +0100928
Harald Weltec91085e2022-02-10 18:05:45 +0100929 # If a script file is specified, be sure that it actually exists
930 if opts.script:
931 if not os.access(opts.script, os.R_OK):
932 print("Invalid script file!")
933 sys.exit(2)
Philipp Maier13e258d2021-04-08 17:48:49 +0200934
Harald Weltec91085e2022-02-10 18:05:45 +0100935 # Register csv-file as card data provider, either from specified CSV
936 # or from CSV file in home directory
937 csv_default = str(Path.home()) + "/.osmocom/pysim/card_data.csv"
938 if opts.csv:
939 card_key_provider_register(CardKeyProviderCsv(opts.csv))
940 if os.path.isfile(csv_default):
941 card_key_provider_register(CardKeyProviderCsv(csv_default))
Philipp Maier2b11c322021-03-17 12:37:39 +0100942
Harald Weltec91085e2022-02-10 18:05:45 +0100943 # Init card reader driver
944 sl = init_reader(opts)
945 if sl is None:
946 exit(1)
Philipp Maierea95c392021-09-16 13:10:19 +0200947
Harald Weltec91085e2022-02-10 18:05:45 +0100948 # Create command layer
949 scc = SimCardCommands(transport=sl)
Philipp Maierea95c392021-09-16 13:10:19 +0200950
Harald Weltec91085e2022-02-10 18:05:45 +0100951 # Create a card handler (for bulk provisioning)
952 if opts.card_handler_config:
953 ch = CardHandlerAuto(None, opts.card_handler_config)
954 else:
955 ch = CardHandler(sl)
Philipp Maier76667642021-09-22 16:53:22 +0200956
Harald Weltec91085e2022-02-10 18:05:45 +0100957 # Detect and initialize the card in the reader. This may fail when there
958 # is no card in the reader or the card is unresponsive. PysimApp is
959 # able to tolerate and recover from that.
960 try:
961 rs, card = init_card(sl)
962 app = PysimApp(card, rs, sl, ch, opts.script)
963 except:
964 print("Card initialization failed with an exception:")
965 print("---------------------8<---------------------")
966 traceback.print_exc()
967 print("---------------------8<---------------------")
968 print("(you may still try to recover from this manually by using the 'equip' command.)")
969 print(
970 " it should also be noted that some readers may behave strangely when no card")
971 print(" is inserted.)")
972 print("")
Philipp Maier7226c092022-06-01 17:58:38 +0200973 app = PysimApp(card, None, sl, ch, opts.script)
Philipp Maierea95c392021-09-16 13:10:19 +0200974
Harald Weltec91085e2022-02-10 18:05:45 +0100975 # If the user supplies an ADM PIN at via commandline args authenticate
976 # immediately so that the user does not have to use the shell commands
977 pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
978 if pin_adm:
979 if not card:
980 print("Card error, cannot do ADM verification with supplied ADM pin now.")
981 try:
982 card.verify_adm(h2b(pin_adm))
983 except Exception as e:
984 print(e)
Philipp Maier228c98e2021-03-10 20:14:06 +0100985
Harald Weltec91085e2022-02-10 18:05:45 +0100986 app.cmdloop()