blob: 831c775c252f7aa3b8a7bf3c9b8d06cde75089a6 [file] [log] [blame]
Harald Welteb2edd142021-01-08 23:29:35 +01001#!/usr/bin/env python3
2
3# Interactive shell for working with SIM / UICC / USIM / ISIM cards
4#
Harald Weltecab26c72022-08-06 16:12:30 +02005# (C) 2021-2022 by Harald Welte <laforge@osmocom.org>
Harald Welteb2edd142021-01-08 23:29:35 +01006#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20from typing import List
21
22import json
Philipp Maier5d698e52021-09-16 13:18:01 +020023import traceback
Harald Welteb2edd142021-01-08 23:29:35 +010024
25import cmd2
Vadim Yanitskiy8f388002022-07-14 19:06:54 +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 Weltecab26c72022-08-06 16:12:30 +020035from pprint import pprint as pp
36
Harald Welteb2edd142021-01-08 23:29:35 +010037from pySim.exceptions import *
38from pySim.commands import SimCardCommands
Harald Weltecab26c72022-08-06 16:12:30 +020039from pySim.transport import init_reader, ApduTracer, argparse_add_reader_args, ProactiveHandler
Philipp Maierbb73e512021-05-05 16:14:00 +020040from pySim.cards import card_detect, SimCard
Harald Welte1e52b0d2022-07-16 11:53:21 +020041from pySim.utils import h2b, swap_nibbles, rpad, b2h, JsonEncoder, bertlv_parse_one, sw_match
Harald Weltecab26c72022-08-06 16:12:30 +020042from pySim.utils import sanitize_pin_adm, tabulate_str_list, boxed_heading_str, Hexstr
Philipp Maier76667642021-09-22 16:53:22 +020043from pySim.card_handler import CardHandler, CardHandlerAuto
Harald Welteb2edd142021-01-08 23:29:35 +010044
Harald Welte1e52b0d2022-07-16 11:53:21 +020045from pySim.filesystem import RuntimeState, CardDF, CardADF, CardModel
Philipp Maiera028c7d2021-11-08 16:12:03 +010046from pySim.profile import CardProfile
Harald Welteb2edd142021-01-08 23:29:35 +010047from pySim.ts_102_221 import CardProfileUICC
Harald Welte3c9b7842021-10-19 21:44:24 +020048from pySim.ts_102_222 import Ts102222Commands
Harald Welte5ce35242021-04-02 20:27:05 +020049from pySim.ts_31_102 import CardApplicationUSIM
50from pySim.ts_31_103 import CardApplicationISIM
Harald Welte95ce6b12021-10-20 18:40:54 +020051from pySim.ara_m import CardApplicationARAM
Harald Welte34eb5042022-02-21 17:19:28 +010052from pySim.global_platform import CardApplicationISD
Harald Welte2a33ad22021-10-20 10:14:18 +020053from pySim.gsm_r import DF_EIRENE
Harald Weltecab26c72022-08-06 16:12:30 +020054from pySim.cat import ProactiveCommand
Harald Welteb2edd142021-01-08 23:29:35 +010055
Harald Welte4c1dca02021-10-14 17:48:25 +020056# we need to import this module so that the SysmocomSJA2 sub-class of
57# CardModel is created, which will add the ATR-based matching and
58# calling of SysmocomSJA2.add_files. See CardModel.apply_matching_models
Harald Weltef44256c2021-10-14 15:53:39 +020059import pySim.sysmocom_sja2
60
Harald Welte4442b3d2021-04-03 09:00:16 +020061from pySim.card_key_provider import CardKeyProviderCsv, card_key_provider_register, card_key_provider_get_field
Philipp Maier2b11c322021-03-17 12:37:39 +010062
Harald Weltec91085e2022-02-10 18:05:45 +010063
Philipp Maierea95c392021-09-16 13:10:19 +020064def init_card(sl):
Harald Weltec91085e2022-02-10 18:05:45 +010065 """
66 Detect card in reader and setup card profile and runtime state. This
67 function must be called at least once on startup. The card and runtime
68 state object (rs) is required for all pySim-shell commands.
69 """
Philipp Maierea95c392021-09-16 13:10:19 +020070
Harald Weltec91085e2022-02-10 18:05:45 +010071 # Wait up to three seconds for a card in reader and try to detect
72 # the card type.
73 print("Waiting for card...")
74 try:
75 sl.wait_for_card(3)
76 except NoCardError:
77 print("No card detected!")
78 return None, None
79 except:
80 print("Card not readable!")
81 return None, None
Philipp Maierea95c392021-09-16 13:10:19 +020082
Philipp Maier24031252022-06-14 16:16:42 +020083 generic_card = False
Harald Weltec91085e2022-02-10 18:05:45 +010084 card = card_detect("auto", scc)
85 if card is None:
86 print("Warning: Could not detect card type - assuming a generic card type...")
87 card = SimCard(scc)
Philipp Maier24031252022-06-14 16:16:42 +020088 generic_card = True
Philipp Maiera028c7d2021-11-08 16:12:03 +010089
Harald Weltec91085e2022-02-10 18:05:45 +010090 profile = CardProfile.pick(scc)
91 if profile is None:
92 print("Unsupported card type!")
Philipp Maier7226c092022-06-01 17:58:38 +020093 return None, card
Philipp Maierea95c392021-09-16 13:10:19 +020094
Philipp Maier24031252022-06-14 16:16:42 +020095 # ETSI TS 102 221, Table 9.3 specifies a default for the PIN key
96 # references, however card manufactures may still decide to pick an
97 # arbitrary key reference. In case we run on a generic card class that is
98 # detected as an UICC, we will pick the key reference that is officially
99 # specified.
100 if generic_card and isinstance(profile, CardProfileUICC):
101 card._adm_chv_num = 0x0A
102
Harald Weltec91085e2022-02-10 18:05:45 +0100103 print("Info: Card is of type: %s" % str(profile))
Philipp Maiera028c7d2021-11-08 16:12:03 +0100104
Harald Weltec91085e2022-02-10 18:05:45 +0100105 # FIXME: This shouln't be here, the profile should add the applications,
106 # however, we cannot simply put his into ts_102_221.py since we would
107 # have to e.g. import CardApplicationUSIM from ts_31_102.py, which already
108 # imports from ts_102_221.py. This means we will end up with a circular
109 # import, which needs to be resolved first.
110 if isinstance(profile, CardProfileUICC):
111 profile.add_application(CardApplicationUSIM())
112 profile.add_application(CardApplicationISIM())
113 profile.add_application(CardApplicationARAM())
Harald Welte34eb5042022-02-21 17:19:28 +0100114 profile.add_application(CardApplicationISD())
Philipp Maiera028c7d2021-11-08 16:12:03 +0100115
Harald Weltec91085e2022-02-10 18:05:45 +0100116 # Create runtime state with card profile
117 rs = RuntimeState(card, profile)
Philipp Maierea95c392021-09-16 13:10:19 +0200118
Harald Weltec91085e2022-02-10 18:05:45 +0100119 # FIXME: This is an GSM-R related file, it needs to be added throughout,
120 # the profile. At the moment we add it for all cards, this won't hurt,
121 # but regular SIM and UICC will not have it and fail to select it.
122 rs.mf.add_file(DF_EIRENE())
Philipp Maierea95c392021-09-16 13:10:19 +0200123
Harald Weltec91085e2022-02-10 18:05:45 +0100124 CardModel.apply_matching_models(scc, rs)
Harald Weltef44256c2021-10-14 15:53:39 +0200125
Harald Weltec91085e2022-02-10 18:05:45 +0100126 # inform the transport that we can do context-specific SW interpretation
127 sl.set_sw_interpreter(rs)
Philipp Maierea95c392021-09-16 13:10:19 +0200128
Harald Weltec91085e2022-02-10 18:05:45 +0100129 return rs, card
130
Philipp Maier2b11c322021-03-17 12:37:39 +0100131
Harald Welteb2edd142021-01-08 23:29:35 +0100132class PysimApp(cmd2.Cmd):
Harald Weltec91085e2022-02-10 18:05:45 +0100133 CUSTOM_CATEGORY = 'pySim Commands'
Philipp Maier76667642021-09-22 16:53:22 +0200134
Harald Weltec91085e2022-02-10 18:05:45 +0100135 def __init__(self, card, rs, sl, ch, script=None):
136 super().__init__(persistent_history_file='~/.pysim_shell_history', allow_cli_args=False,
Vadim Yanitskiyd5c1bec2022-02-13 22:25:16 +0600137 auto_load_commands=False, startup_script=script)
Vadim Yanitskiy8f388002022-07-14 19:06:54 +0700138 self.intro = style('Welcome to pySim-shell!', fg=Fg.RED)
Harald Weltec91085e2022-02-10 18:05:45 +0100139 self.default_category = 'pySim-shell built-in commands'
140 self.card = None
141 self.rs = None
Harald Weltea6c0f882022-07-17 14:23:17 +0200142 self.lchan = None
143 self.py_locals = {'card': self.card, 'rs': self.rs, 'lchan': self.lchan}
Harald Weltec91085e2022-02-10 18:05:45 +0100144 self.sl = sl
145 self.ch = ch
Harald Welte1748b932021-04-06 21:12:25 +0200146
Harald Weltec91085e2022-02-10 18:05:45 +0100147 self.numeric_path = False
Vadim Yanitskiyd5c1bec2022-02-13 22:25:16 +0600148 self.add_settable(cmd2.Settable('numeric_path', bool, 'Print File IDs instead of names', self,
Harald Weltec91085e2022-02-10 18:05:45 +0100149 onchange_cb=self._onchange_numeric_path))
150 self.conserve_write = True
Vadim Yanitskiyd5c1bec2022-02-13 22:25:16 +0600151 self.add_settable(cmd2.Settable('conserve_write', bool, 'Read and compare before write', self,
Harald Weltec91085e2022-02-10 18:05:45 +0100152 onchange_cb=self._onchange_conserve_write))
153 self.json_pretty_print = True
154 self.add_settable(cmd2.Settable('json_pretty_print',
Vadim Yanitskiyd5c1bec2022-02-13 22:25:16 +0600155 bool, 'Pretty-Print JSON output', self))
Harald Weltec91085e2022-02-10 18:05:45 +0100156 self.apdu_trace = False
Vadim Yanitskiyd5c1bec2022-02-13 22:25:16 +0600157 self.add_settable(cmd2.Settable('apdu_trace', bool, 'Trace and display APDUs exchanged with card', self,
Harald Weltec91085e2022-02-10 18:05:45 +0100158 onchange_cb=self._onchange_apdu_trace))
Philipp Maier5d698e52021-09-16 13:18:01 +0200159
Harald Weltec91085e2022-02-10 18:05:45 +0100160 self.equip(card, rs)
Philipp Maier5d698e52021-09-16 13:18:01 +0200161
Harald Weltec91085e2022-02-10 18:05:45 +0100162 def equip(self, card, rs):
163 """
164 Equip pySim-shell with the supplied card and runtime state, add (or remove) all required settables and
165 and commands to enable card operations.
166 """
Philipp Maier76667642021-09-22 16:53:22 +0200167
Harald Weltec91085e2022-02-10 18:05:45 +0100168 rc = False
Philipp Maier5d698e52021-09-16 13:18:01 +0200169
Harald Weltec91085e2022-02-10 18:05:45 +0100170 # Unequip everything from pySim-shell that would not work in unequipped state
171 if self.rs:
Harald Weltea6c0f882022-07-17 14:23:17 +0200172 lchan = self.rs.lchan[0]
173 lchan.unregister_cmds(self)
Harald Weltec91085e2022-02-10 18:05:45 +0100174 for cmds in [Iso7816Commands, PySimCommands]:
175 cmd_set = self.find_commandsets(cmds)
176 if cmd_set:
177 self.unregister_command_set(cmd_set[0])
Philipp Maier5d698e52021-09-16 13:18:01 +0200178
Harald Weltec91085e2022-02-10 18:05:45 +0100179 self.card = card
180 self.rs = rs
Philipp Maier5d698e52021-09-16 13:18:01 +0200181
Harald Weltec91085e2022-02-10 18:05:45 +0100182 # When a card object and a runtime state is present, (re)equip pySim-shell with everything that is
183 # needed to operate on cards.
184 if self.card and self.rs:
Harald Weltea6c0f882022-07-17 14:23:17 +0200185 self.lchan = self.rs.lchan[0]
Harald Weltec91085e2022-02-10 18:05:45 +0100186 self._onchange_conserve_write(
187 'conserve_write', False, self.conserve_write)
188 self._onchange_apdu_trace('apdu_trace', False, self.apdu_trace)
189 self.register_command_set(Iso7816Commands())
Harald Welte3c9b7842021-10-19 21:44:24 +0200190 self.register_command_set(Ts102222Commands())
Harald Weltec91085e2022-02-10 18:05:45 +0100191 self.register_command_set(PySimCommands())
192 self.iccid, sw = self.card.read_iccid()
Harald Weltea6c0f882022-07-17 14:23:17 +0200193 self.lchan.select('MF', self)
Harald Weltec91085e2022-02-10 18:05:45 +0100194 rc = True
195 else:
196 self.poutput("pySim-shell not equipped!")
Philipp Maier5d698e52021-09-16 13:18:01 +0200197
Harald Weltec91085e2022-02-10 18:05:45 +0100198 self.update_prompt()
199 return rc
Harald Welteb2edd142021-01-08 23:29:35 +0100200
Harald Weltec91085e2022-02-10 18:05:45 +0100201 def poutput_json(self, data, force_no_pretty=False):
202 """like cmd2.poutput() but for a JSON serializable dict."""
203 if force_no_pretty or self.json_pretty_print == False:
204 output = json.dumps(data, cls=JsonEncoder)
205 else:
206 output = json.dumps(data, cls=JsonEncoder, indent=4)
207 self.poutput(output)
Harald Welteb2edd142021-01-08 23:29:35 +0100208
Harald Weltec91085e2022-02-10 18:05:45 +0100209 def _onchange_numeric_path(self, param_name, old, new):
210 self.update_prompt()
Philipp Maier38c74f62021-03-17 17:19:52 +0100211
Harald Weltec91085e2022-02-10 18:05:45 +0100212 def _onchange_conserve_write(self, param_name, old, new):
213 if self.rs:
214 self.rs.conserve_write = new
Harald Welte7829d8a2021-04-10 11:28:53 +0200215
Harald Weltec91085e2022-02-10 18:05:45 +0100216 def _onchange_apdu_trace(self, param_name, old, new):
217 if self.card:
218 if new == True:
219 self.card._scc._tp.apdu_tracer = self.Cmd2ApduTracer(self)
220 else:
221 self.card._scc._tp.apdu_tracer = None
Harald Welte7829d8a2021-04-10 11:28:53 +0200222
Harald Weltec91085e2022-02-10 18:05:45 +0100223 class Cmd2ApduTracer(ApduTracer):
224 def __init__(self, cmd2_app):
225 self.cmd2 = app
Harald Welte7829d8a2021-04-10 11:28:53 +0200226
Harald Weltec91085e2022-02-10 18:05:45 +0100227 def trace_response(self, cmd, sw, resp):
228 self.cmd2.poutput("-> %s %s" % (cmd[:10], cmd[10:]))
229 self.cmd2.poutput("<- %s: %s" % (sw, resp))
Harald Welteb2edd142021-01-08 23:29:35 +0100230
Harald Weltec91085e2022-02-10 18:05:45 +0100231 def update_prompt(self):
Harald Weltea6c0f882022-07-17 14:23:17 +0200232 if self.lchan:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200233 path_str = self.lchan.selected_file.fully_qualified_path_str(not self.numeric_path)
234 self.prompt = 'pySIM-shell (%s)> ' % (path_str)
Harald Weltec91085e2022-02-10 18:05:45 +0100235 else:
Philipp Maier7226c092022-06-01 17:58:38 +0200236 if self.card:
237 self.prompt = 'pySIM-shell (no card profile)> '
238 else:
239 self.prompt = 'pySIM-shell (no card)> '
Harald Welteb2edd142021-01-08 23:29:35 +0100240
Harald Weltec91085e2022-02-10 18:05:45 +0100241 @cmd2.with_category(CUSTOM_CATEGORY)
242 def do_intro(self, _):
243 """Display the intro banner"""
244 self.poutput(self.intro)
Philipp Maier9764de22021-11-03 10:44:39 +0100245
Harald Weltec91085e2022-02-10 18:05:45 +0100246 def do_eof(self, _: argparse.Namespace) -> bool:
247 self.poutput("")
248 return self.do_quit('')
Philipp Maier5d698e52021-09-16 13:18:01 +0200249
Harald Weltec91085e2022-02-10 18:05:45 +0100250 @cmd2.with_category(CUSTOM_CATEGORY)
251 def do_equip(self, opts):
252 """Equip pySim-shell with card"""
253 rs, card = init_card(sl)
254 self.equip(card, rs)
Philipp Maier76667642021-09-22 16:53:22 +0200255
Philipp Maier7226c092022-06-01 17:58:38 +0200256 apdu_cmd_parser = argparse.ArgumentParser()
257 apdu_cmd_parser.add_argument('APDU', type=str, help='APDU as hex string')
Philipp Maiere7d1b672022-06-01 18:05:34 +0200258 apdu_cmd_parser.add_argument('--expect-sw', help='expect a specified status word', type=str, default=None)
Philipp Maier7226c092022-06-01 17:58:38 +0200259
260 @cmd2.with_argparser(apdu_cmd_parser)
261 def do_apdu(self, opts):
262 """Send a raw APDU to the card, and print SW + Response.
263 DANGEROUS: pySim-shell will not know any card state changes, and
264 not continue to work as expected if you e.g. select a different
265 file."""
266 data, sw = self.card._scc._tp.send_apdu(opts.APDU)
267 if data:
268 self.poutput("SW: %s, RESP: %s" % (sw, data))
269 else:
270 self.poutput("SW: %s" % sw)
Philipp Maiere7d1b672022-06-01 18:05:34 +0200271 if opts.expect_sw:
272 if not sw_match(sw, opts.expect_sw):
273 raise SwMatchError(sw, opts.expect_sw)
Philipp Maier7226c092022-06-01 17:58:38 +0200274
Harald Weltec91085e2022-02-10 18:05:45 +0100275 class InterceptStderr(list):
276 def __init__(self):
277 self._stderr_backup = sys.stderr
Philipp Maier76667642021-09-22 16:53:22 +0200278
Harald Weltec91085e2022-02-10 18:05:45 +0100279 def __enter__(self):
280 self._stringio_stderr = StringIO()
281 sys.stderr = self._stringio_stderr
282 return self
Philipp Maier76667642021-09-22 16:53:22 +0200283
Harald Weltec91085e2022-02-10 18:05:45 +0100284 def __exit__(self, *args):
285 self.stderr = self._stringio_stderr.getvalue().strip()
286 del self._stringio_stderr
287 sys.stderr = self._stderr_backup
Philipp Maier76667642021-09-22 16:53:22 +0200288
Harald Weltec91085e2022-02-10 18:05:45 +0100289 def _show_failure_sign(self):
Vadim Yanitskiy8f388002022-07-14 19:06:54 +0700290 self.poutput(style(" +-------------+", fg=Fg.LIGHT_RED))
291 self.poutput(style(" + ## ## +", fg=Fg.LIGHT_RED))
292 self.poutput(style(" + ## ## +", fg=Fg.LIGHT_RED))
293 self.poutput(style(" + ### +", fg=Fg.LIGHT_RED))
294 self.poutput(style(" + ## ## +", fg=Fg.LIGHT_RED))
295 self.poutput(style(" + ## ## +", fg=Fg.LIGHT_RED))
296 self.poutput(style(" +-------------+", fg=Fg.LIGHT_RED))
Harald Weltec91085e2022-02-10 18:05:45 +0100297 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200298
Harald Weltec91085e2022-02-10 18:05:45 +0100299 def _show_success_sign(self):
Vadim Yanitskiy8f388002022-07-14 19:06:54 +0700300 self.poutput(style(" +-------------+", fg=Fg.LIGHT_GREEN))
301 self.poutput(style(" + ## +", fg=Fg.LIGHT_GREEN))
302 self.poutput(style(" + ## +", fg=Fg.LIGHT_GREEN))
303 self.poutput(style(" + # ## +", fg=Fg.LIGHT_GREEN))
304 self.poutput(style(" + ## # +", fg=Fg.LIGHT_GREEN))
305 self.poutput(style(" + ## +", fg=Fg.LIGHT_GREEN))
306 self.poutput(style(" +-------------+", fg=Fg.LIGHT_GREEN))
Harald Weltec91085e2022-02-10 18:05:45 +0100307 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200308
Harald Weltec91085e2022-02-10 18:05:45 +0100309 def _process_card(self, first, script_path):
Philipp Maier76667642021-09-22 16:53:22 +0200310
Harald Weltec91085e2022-02-10 18:05:45 +0100311 # Early phase of card initialzation (this part may fail with an exception)
312 try:
313 rs, card = init_card(self.sl)
314 rc = self.equip(card, rs)
315 except:
316 self.poutput("")
317 self.poutput("Card initialization failed with an exception:")
318 self.poutput("---------------------8<---------------------")
319 traceback.print_exc()
320 self.poutput("---------------------8<---------------------")
321 self.poutput("")
322 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200323
Harald Weltec91085e2022-02-10 18:05:45 +0100324 # Actual card processing step. This part should never fail with an exception since the cmd2
325 # do_run_script method will catch any exception that might occur during script execution.
326 if rc:
327 self.poutput("")
328 self.poutput("Transcript stdout:")
329 self.poutput("---------------------8<---------------------")
330 with self.InterceptStderr() as logged:
331 self.do_run_script(script_path)
332 self.poutput("---------------------8<---------------------")
Philipp Maier76667642021-09-22 16:53:22 +0200333
Harald Weltec91085e2022-02-10 18:05:45 +0100334 self.poutput("")
335 self.poutput("Transcript stderr:")
336 if logged.stderr:
337 self.poutput("---------------------8<---------------------")
338 self.poutput(logged.stderr)
339 self.poutput("---------------------8<---------------------")
340 else:
341 self.poutput("(none)")
Philipp Maier76667642021-09-22 16:53:22 +0200342
Harald Weltec91085e2022-02-10 18:05:45 +0100343 # Check for exceptions
344 self.poutput("")
345 if "EXCEPTION of type" not in logged.stderr:
346 return 0
Philipp Maier76667642021-09-22 16:53:22 +0200347
Harald Weltec91085e2022-02-10 18:05:45 +0100348 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200349
Harald Weltec91085e2022-02-10 18:05:45 +0100350 bulk_script_parser = argparse.ArgumentParser()
351 bulk_script_parser.add_argument(
352 'script_path', help="path to the script file")
353 bulk_script_parser.add_argument('--halt_on_error', help='stop card handling if an exeption occurs',
354 action='store_true')
355 bulk_script_parser.add_argument('--tries', type=int, default=2,
356 help='how many tries before trying the next card')
357 bulk_script_parser.add_argument('--on_stop_action', type=str, default=None,
358 help='commandline to execute when card handling has stopped')
359 bulk_script_parser.add_argument('--pre_card_action', type=str, default=None,
360 help='commandline to execute before actually talking to the card')
Philipp Maier76667642021-09-22 16:53:22 +0200361
Harald Weltec91085e2022-02-10 18:05:45 +0100362 @cmd2.with_argparser(bulk_script_parser)
363 @cmd2.with_category(CUSTOM_CATEGORY)
364 def do_bulk_script(self, opts):
365 """Run script on multiple cards (bulk provisioning)"""
Philipp Maier76667642021-09-22 16:53:22 +0200366
Harald Weltec91085e2022-02-10 18:05:45 +0100367 # Make sure that the script file exists and that it is readable.
368 if not os.access(opts.script_path, os.R_OK):
369 self.poutput("Invalid script file!")
370 return
Philipp Maier76667642021-09-22 16:53:22 +0200371
Harald Weltec91085e2022-02-10 18:05:45 +0100372 success_count = 0
373 fail_count = 0
Philipp Maier76667642021-09-22 16:53:22 +0200374
Harald Weltec91085e2022-02-10 18:05:45 +0100375 first = True
376 while 1:
377 # TODO: Count consecutive failures, if more than N consecutive failures occur, then stop.
378 # The ratinale is: There may be a problem with the device, we do want to prevent that
379 # all remaining cards are fired to the error bin. This is only relevant for situations
380 # with large stacks, probably we do not need this feature right now.
Philipp Maier76667642021-09-22 16:53:22 +0200381
Harald Weltec91085e2022-02-10 18:05:45 +0100382 try:
383 # In case of failure, try multiple times.
384 for i in range(opts.tries):
385 # fetch card into reader bay
386 ch.get(first)
Philipp Maier76667642021-09-22 16:53:22 +0200387
Harald Weltec91085e2022-02-10 18:05:45 +0100388 # if necessary execute an action before we start processing the card
389 if(opts.pre_card_action):
390 os.system(opts.pre_card_action)
Philipp Maier76667642021-09-22 16:53:22 +0200391
Harald Weltec91085e2022-02-10 18:05:45 +0100392 # process the card
393 rc = self._process_card(first, opts.script_path)
394 if rc == 0:
395 success_count = success_count + 1
396 self._show_success_sign()
397 self.poutput("Statistics: success :%i, failure: %i" % (
398 success_count, fail_count))
399 break
400 else:
401 fail_count = fail_count + 1
402 self._show_failure_sign()
403 self.poutput("Statistics: success :%i, failure: %i" % (
404 success_count, fail_count))
Philipp Maier76667642021-09-22 16:53:22 +0200405
Harald Weltec91085e2022-02-10 18:05:45 +0100406 # Depending on success or failure, the card goes either in the "error" bin or in the
407 # "done" bin.
408 if rc < 0:
409 ch.error()
410 else:
411 ch.done()
Philipp Maier76667642021-09-22 16:53:22 +0200412
Harald Weltec91085e2022-02-10 18:05:45 +0100413 # In most cases it is possible to proceed with the next card, but the
414 # user may decide to halt immediately when an error occurs
415 if opts.halt_on_error and rc < 0:
416 return
Philipp Maier76667642021-09-22 16:53:22 +0200417
Harald Weltec91085e2022-02-10 18:05:45 +0100418 except (KeyboardInterrupt):
419 self.poutput("")
420 self.poutput("Terminated by user!")
421 return
422 except (SystemExit):
423 # When all cards are processed the card handler device will throw a SystemExit
424 # exception. Also Errors that are not recoverable (cards stuck etc.) will end up here.
425 # The user has the option to execute some action to make aware that the card handler
426 # needs service.
427 if(opts.on_stop_action):
428 os.system(opts.on_stop_action)
429 return
430 except:
431 self.poutput("")
432 self.poutput("Card handling failed with an exception:")
433 self.poutput("---------------------8<---------------------")
434 traceback.print_exc()
435 self.poutput("---------------------8<---------------------")
436 self.poutput("")
437 fail_count = fail_count + 1
438 self._show_failure_sign()
439 self.poutput("Statistics: success :%i, failure: %i" %
440 (success_count, fail_count))
Philipp Maierb52feed2021-09-22 16:43:13 +0200441
Harald Weltec91085e2022-02-10 18:05:45 +0100442 first = False
443
444 echo_parser = argparse.ArgumentParser()
445 echo_parser.add_argument('string', help="string to echo on the shell")
446
447 @cmd2.with_argparser(echo_parser)
448 @cmd2.with_category(CUSTOM_CATEGORY)
449 def do_echo(self, opts):
450 """Echo (print) a string on the console"""
451 self.poutput(opts.string)
452
Harald Weltefc315482022-07-23 12:49:14 +0200453 @cmd2.with_category(CUSTOM_CATEGORY)
454 def do_version(self, opts):
455 """Print the pySim software version."""
456 import pkg_resources
457 self.poutput(pkg_resources.get_distribution('pySim'))
Harald Welteb2edd142021-01-08 23:29:35 +0100458
Harald Welte31d2cf02021-04-03 10:47:29 +0200459@with_default_category('pySim Commands')
460class PySimCommands(CommandSet):
Harald Weltec91085e2022-02-10 18:05:45 +0100461 def __init__(self):
462 super().__init__()
Harald Welteb2edd142021-01-08 23:29:35 +0100463
Harald Weltec91085e2022-02-10 18:05:45 +0100464 dir_parser = argparse.ArgumentParser()
465 dir_parser.add_argument(
466 '--fids', help='Show file identifiers', action='store_true')
467 dir_parser.add_argument(
468 '--names', help='Show file names', action='store_true')
469 dir_parser.add_argument(
470 '--apps', help='Show applications', action='store_true')
471 dir_parser.add_argument(
472 '--all', help='Show all selectable identifiers and names', action='store_true')
Philipp Maier5d3e2592021-02-22 17:22:16 +0100473
Harald Weltec91085e2022-02-10 18:05:45 +0100474 @cmd2.with_argparser(dir_parser)
475 def do_dir(self, opts):
476 """Show a listing of files available in currently selected DF or MF"""
477 if opts.all:
478 flags = []
479 elif opts.fids or opts.names or opts.apps:
480 flags = ['PARENT', 'SELF']
481 if opts.fids:
482 flags += ['FIDS', 'AIDS']
483 if opts.names:
484 flags += ['FNAMES', 'ANAMES']
485 if opts.apps:
486 flags += ['ANAMES', 'AIDS']
487 else:
488 flags = ['PARENT', 'SELF', 'FNAMES', 'ANAMES']
489 selectables = list(
Harald Weltea6c0f882022-07-17 14:23:17 +0200490 self._cmd.lchan.selected_file.get_selectable_names(flags=flags))
Harald Weltec91085e2022-02-10 18:05:45 +0100491 directory_str = tabulate_str_list(
492 selectables, width=79, hspace=2, lspace=1, align_left=True)
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200493 path = self._cmd.lchan.selected_file.fully_qualified_path_str(True)
494 self._cmd.poutput(path)
495 path = self._cmd.lchan.selected_file.fully_qualified_path_str(False)
496 self._cmd.poutput(path)
Harald Weltec91085e2022-02-10 18:05:45 +0100497 self._cmd.poutput(directory_str)
498 self._cmd.poutput("%d files" % len(selectables))
Harald Welteb2edd142021-01-08 23:29:35 +0100499
Philipp Maier7b138b02022-05-31 13:42:56 +0200500 def walk(self, indent=0, action_ef=None, action_df=None, context=None, **kwargs):
Harald Weltec91085e2022-02-10 18:05:45 +0100501 """Recursively walk through the file system, starting at the currently selected DF"""
Philipp Maier7b138b02022-05-31 13:42:56 +0200502
Harald Weltea6c0f882022-07-17 14:23:17 +0200503 if isinstance(self._cmd.lchan.selected_file, CardDF):
Philipp Maier7b138b02022-05-31 13:42:56 +0200504 if action_df:
505 action_df(context, opts)
506
Harald Weltea6c0f882022-07-17 14:23:17 +0200507 files = self._cmd.lchan.selected_file.get_selectables(
Harald Weltec91085e2022-02-10 18:05:45 +0100508 flags=['FNAMES', 'ANAMES'])
509 for f in files:
Philipp Maier7b138b02022-05-31 13:42:56 +0200510 # special case: When no action is performed, just output a directory
511 if not action_ef and not action_df:
Harald Weltec91085e2022-02-10 18:05:45 +0100512 output_str = " " * indent + str(f) + (" " * 250)
513 output_str = output_str[0:25]
514 if isinstance(files[f], CardADF):
515 output_str += " " + str(files[f].aid)
516 else:
517 output_str += " " + str(files[f].fid)
518 output_str += " " + str(files[f].desc)
519 self._cmd.poutput(output_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200520
Harald Weltec91085e2022-02-10 18:05:45 +0100521 if isinstance(files[f], CardDF):
522 skip_df = False
523 try:
Harald Weltea6c0f882022-07-17 14:23:17 +0200524 fcp_dec = self._cmd.lchan.select(f, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100525 except Exception as e:
526 skip_df = True
Harald Weltea6c0f882022-07-17 14:23:17 +0200527 df = self._cmd.lchan.selected_file
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200528 df_path = df.fully_qualified_path_str(True)
529 df_skip_reason_str = df_path + \
Harald Weltec91085e2022-02-10 18:05:45 +0100530 "/" + str(f) + ", " + str(e)
531 if context:
532 context['DF_SKIP'] += 1
533 context['DF_SKIP_REASON'].append(df_skip_reason_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200534
Harald Weltec91085e2022-02-10 18:05:45 +0100535 # If the DF was skipped, we never have entered the directory
536 # below, so we must not move up.
537 if skip_df == False:
Philipp Maier7b138b02022-05-31 13:42:56 +0200538 self.walk(indent + 1, action_ef, action_df, context, **kwargs)
Harald Weltea6c0f882022-07-17 14:23:17 +0200539 fcp_dec = self._cmd.lchan.select("..", self._cmd)
Philipp Maierf408a402021-04-09 21:16:12 +0200540
Philipp Maier7b138b02022-05-31 13:42:56 +0200541 elif action_ef:
Harald Weltea6c0f882022-07-17 14:23:17 +0200542 df_before_action = self._cmd.lchan.selected_file
Philipp Maier7b138b02022-05-31 13:42:56 +0200543 action_ef(f, context, **kwargs)
Harald Weltec91085e2022-02-10 18:05:45 +0100544 # When walking through the file system tree the action must not
545 # always restore the currently selected file to the file that
546 # was selected before executing the action() callback.
Harald Weltea6c0f882022-07-17 14:23:17 +0200547 if df_before_action != self._cmd.lchan.selected_file:
Harald Weltec91085e2022-02-10 18:05:45 +0100548 raise RuntimeError("inconsistent walk, %s is currently selected but expecting %s to be selected"
Harald Weltea6c0f882022-07-17 14:23:17 +0200549 % (str(self._cmd.lchan.selected_file), str(df_before_action)))
Philipp Maierff9dae22021-02-25 17:03:21 +0100550
Harald Weltec91085e2022-02-10 18:05:45 +0100551 def do_tree(self, opts):
552 """Display a filesystem-tree with all selectable files"""
553 self.walk()
Philipp Maierff9dae22021-02-25 17:03:21 +0100554
Philipp Maier7b138b02022-05-31 13:42:56 +0200555 def export_ef(self, filename, context, as_json):
556 """ Select and export a single elementary file (EF) """
Harald Weltec91085e2022-02-10 18:05:45 +0100557 context['COUNT'] += 1
Harald Weltea6c0f882022-07-17 14:23:17 +0200558 df = self._cmd.lchan.selected_file
Philipp Maierac34dcc2021-04-01 17:19:05 +0200559
Philipp Maierea81f752022-05-19 10:13:30 +0200560 # The currently selected file (not the file we are going to export)
561 # must always be an ADF or DF. From this starting point we select
562 # the EF we want to export. To maintain consistency we will then
563 # select the current DF again (see comment below).
Harald Weltec91085e2022-02-10 18:05:45 +0100564 if not isinstance(df, CardDF):
565 raise RuntimeError(
566 "currently selected file %s is not a DF or ADF" % str(df))
Philipp Maierac34dcc2021-04-01 17:19:05 +0200567
Harald Weltec91085e2022-02-10 18:05:45 +0100568 df_path_list = df.fully_qualified_path(True)
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200569 df_path = df.fully_qualified_path_str(True)
570 df_path_fid = df.fully_qualified_path_str(False)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100571
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200572 file_str = df_path + "/" + str(filename)
Harald Weltec91085e2022-02-10 18:05:45 +0100573 self._cmd.poutput(boxed_heading_str(file_str))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100574
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200575 self._cmd.poutput("# directory: %s (%s)" % (df_path, df_path_fid))
Harald Weltec91085e2022-02-10 18:05:45 +0100576 try:
Harald Weltea6c0f882022-07-17 14:23:17 +0200577 fcp_dec = self._cmd.lchan.select(filename, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100578 self._cmd.poutput("# file: %s (%s)" % (
Harald Weltea6c0f882022-07-17 14:23:17 +0200579 self._cmd.lchan.selected_file.name, self._cmd.lchan.selected_file.fid))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100580
Harald Weltea6c0f882022-07-17 14:23:17 +0200581 structure = self._cmd.lchan.selected_file_structure()
Harald Weltec91085e2022-02-10 18:05:45 +0100582 self._cmd.poutput("# structure: %s" % str(structure))
Harald Weltea6c0f882022-07-17 14:23:17 +0200583 self._cmd.poutput("# RAW FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp_hex))
584 self._cmd.poutput("# Decoded FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100585
Harald Weltec91085e2022-02-10 18:05:45 +0100586 for f in df_path_list:
587 self._cmd.poutput("select " + str(f))
Harald Weltea6c0f882022-07-17 14:23:17 +0200588 self._cmd.poutput("select " + self._cmd.lchan.selected_file.name)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100589
Harald Weltec91085e2022-02-10 18:05:45 +0100590 if structure == 'transparent':
Harald Welte08b11ab2022-02-10 18:56:41 +0100591 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200592 result = self._cmd.lchan.read_binary_dec()
Harald Welte08b11ab2022-02-10 18:56:41 +0100593 self._cmd.poutput("update_binary_decoded '%s'" % json.dumps(result[0], cls=JsonEncoder))
594 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200595 result = self._cmd.lchan.read_binary()
Harald Welte08b11ab2022-02-10 18:56:41 +0100596 self._cmd.poutput("update_binary " + str(result[0]))
Harald Weltec91085e2022-02-10 18:05:45 +0100597 elif structure == 'cyclic' or structure == 'linear_fixed':
598 # Use number of records specified in select response
Harald Weltea6c0f882022-07-17 14:23:17 +0200599 num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
Harald Welte747a9782022-02-13 17:52:28 +0100600 if num_of_rec:
Harald Weltec91085e2022-02-10 18:05:45 +0100601 for r in range(1, num_of_rec + 1):
Harald Welte08b11ab2022-02-10 18:56:41 +0100602 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200603 result = self._cmd.lchan.read_record_dec(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100604 self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
605 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200606 result = self._cmd.lchan.read_record(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100607 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
608
Harald Weltec91085e2022-02-10 18:05:45 +0100609 # When the select response does not return the number of records, read until we hit the
610 # first record that cannot be read.
611 else:
612 r = 1
613 while True:
614 try:
Harald Welte08b11ab2022-02-10 18:56:41 +0100615 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200616 result = self._cmd.lchan.read_record_dec(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100617 self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
618 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200619 result = self._cmd.lchan.read_record(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100620 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
Harald Weltec91085e2022-02-10 18:05:45 +0100621 except SwMatchError as e:
622 # We are past the last valid record - stop
623 if e.sw_actual == "9402":
624 break
625 # Some other problem occurred
626 else:
627 raise e
Harald Weltec91085e2022-02-10 18:05:45 +0100628 r = r + 1
629 elif structure == 'ber_tlv':
Harald Weltea6c0f882022-07-17 14:23:17 +0200630 tags = self._cmd.lchan.retrieve_tags()
Harald Weltec91085e2022-02-10 18:05:45 +0100631 for t in tags:
Harald Weltea6c0f882022-07-17 14:23:17 +0200632 result = self._cmd.lchan.retrieve_data(t)
Harald Weltec91085e2022-02-10 18:05:45 +0100633 (tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
634 self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val)))
635 else:
636 raise RuntimeError(
637 'Unsupported structure "%s" of file "%s"' % (structure, filename))
638 except Exception as e:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200639 bad_file_str = df_path + "/" + str(filename) + ", " + str(e)
Harald Weltec91085e2022-02-10 18:05:45 +0100640 self._cmd.poutput("# bad file: %s" % bad_file_str)
641 context['ERR'] += 1
642 context['BAD'].append(bad_file_str)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100643
Harald Weltec91085e2022-02-10 18:05:45 +0100644 # When reading the file is done, make sure the parent file is
645 # selected again. This will be the usual case, however we need
646 # to check before since we must not select the same DF twice
Harald Weltea6c0f882022-07-17 14:23:17 +0200647 if df != self._cmd.lchan.selected_file:
648 self._cmd.lchan.select(df.fid or df.aid, self._cmd)
Philipp Maierac34dcc2021-04-01 17:19:05 +0200649
Harald Weltec91085e2022-02-10 18:05:45 +0100650 self._cmd.poutput("#")
Philipp Maier24f7bd32021-02-25 17:06:18 +0100651
Harald Weltec91085e2022-02-10 18:05:45 +0100652 export_parser = argparse.ArgumentParser()
653 export_parser.add_argument(
654 '--filename', type=str, default=None, help='only export specific file')
Harald Welte08b11ab2022-02-10 18:56:41 +0100655 export_parser.add_argument(
656 '--json', action='store_true', help='export as JSON (less reliable)')
Philipp Maier24f7bd32021-02-25 17:06:18 +0100657
Harald Weltec91085e2022-02-10 18:05:45 +0100658 @cmd2.with_argparser(export_parser)
659 def do_export(self, opts):
660 """Export files to script that can be imported back later"""
661 context = {'ERR': 0, 'COUNT': 0, 'BAD': [],
662 'DF_SKIP': 0, 'DF_SKIP_REASON': []}
Philipp Maier9a4091d2022-05-19 10:20:30 +0200663 kwargs_export = {'as_json': opts.json}
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200664 exception_str_add = ""
665
Harald Weltec91085e2022-02-10 18:05:45 +0100666 if opts.filename:
Philipp Maier7b138b02022-05-31 13:42:56 +0200667 self.export_ef(opts.filename, context, **kwargs_export)
Harald Weltec91085e2022-02-10 18:05:45 +0100668 else:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200669 try:
670 self.walk(0, self.export_ef, None, context, **kwargs_export)
671 except Exception as e:
672 print("# Stopping early here due to exception: " + str(e))
673 print("#")
674 exception_str_add = ", also had to stop early due to exception:" + str(e)
Philipp Maier80ce71f2021-04-19 21:24:23 +0200675
Harald Weltec91085e2022-02-10 18:05:45 +0100676 self._cmd.poutput(boxed_heading_str("Export summary"))
Philipp Maier80ce71f2021-04-19 21:24:23 +0200677
Harald Weltec91085e2022-02-10 18:05:45 +0100678 self._cmd.poutput("# total files visited: %u" % context['COUNT'])
679 self._cmd.poutput("# bad files: %u" % context['ERR'])
680 for b in context['BAD']:
681 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200682
Harald Weltec91085e2022-02-10 18:05:45 +0100683 self._cmd.poutput("# skipped dedicated files(s): %u" %
684 context['DF_SKIP'])
685 for b in context['DF_SKIP_REASON']:
686 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200687
Harald Weltec91085e2022-02-10 18:05:45 +0100688 if context['ERR'] and context['DF_SKIP']:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200689 raise RuntimeError("unable to export %i elementary file(s) and %i dedicated file(s)%s" % (
690 context['ERR'], context['DF_SKIP'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100691 elif context['ERR']:
692 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200693 "unable to export %i elementary file(s)%s" % (context['ERR'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100694 elif context['DF_SKIP']:
695 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200696 "unable to export %i dedicated files(s)%s" % (context['ERR'], exception_str_add))
Harald Welteb2edd142021-01-08 23:29:35 +0100697
Harald Weltec91085e2022-02-10 18:05:45 +0100698 def do_reset(self, opts):
699 """Reset the Card."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200700 atr = self._cmd.lchan.reset(self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100701 self._cmd.poutput('Card ATR: %s' % atr)
702 self._cmd.update_prompt()
Harald Weltedaf2b392021-05-03 23:17:29 +0200703
Harald Weltec91085e2022-02-10 18:05:45 +0100704 def do_desc(self, opts):
705 """Display human readable file description for the currently selected file"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200706 desc = self._cmd.lchan.selected_file.desc
Harald Weltec91085e2022-02-10 18:05:45 +0100707 if desc:
708 self._cmd.poutput(desc)
709 else:
710 self._cmd.poutput("no description available")
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200711
Harald Weltec91085e2022-02-10 18:05:45 +0100712 def do_verify_adm(self, arg):
713 """VERIFY the ADM1 PIN"""
714 if arg:
715 # use specified ADM-PIN
716 pin_adm = sanitize_pin_adm(arg)
717 else:
718 # try to find an ADM-PIN if none is specified
719 result = card_key_provider_get_field(
720 'ADM1', key='ICCID', value=self._cmd.iccid)
721 pin_adm = sanitize_pin_adm(result)
722 if pin_adm:
723 self._cmd.poutput(
724 "found ADM-PIN '%s' for ICCID '%s'" % (result, self._cmd.iccid))
725 else:
726 raise ValueError(
727 "cannot find ADM-PIN for ICCID '%s'" % (self._cmd.iccid))
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200728
Harald Weltec91085e2022-02-10 18:05:45 +0100729 if pin_adm:
730 self._cmd.card.verify_adm(h2b(pin_adm))
731 else:
732 raise ValueError("error: cannot authenticate, no adm-pin!")
733
Harald Welteb2edd142021-01-08 23:29:35 +0100734
Harald Welte31d2cf02021-04-03 10:47:29 +0200735@with_default_category('ISO7816 Commands')
736class Iso7816Commands(CommandSet):
Harald Weltec91085e2022-02-10 18:05:45 +0100737 def __init__(self):
738 super().__init__()
Harald Welte31d2cf02021-04-03 10:47:29 +0200739
Harald Weltec91085e2022-02-10 18:05:45 +0100740 def do_select(self, opts):
741 """SELECT a File (ADF/DF/EF)"""
742 if len(opts.arg_list) == 0:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200743 path = self._cmd.lchan.selected_file.fully_qualified_path_str(True)
744 path_fid = self._cmd.lchan.selected_file.fully_qualified_path_str(False)
745 self._cmd.poutput("currently selected file: %s (%s)" % (path, path_fid))
Harald Weltec91085e2022-02-10 18:05:45 +0100746 return
Harald Welte31d2cf02021-04-03 10:47:29 +0200747
Harald Weltec91085e2022-02-10 18:05:45 +0100748 path = opts.arg_list[0]
Harald Weltea6c0f882022-07-17 14:23:17 +0200749 fcp_dec = self._cmd.lchan.select(path, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100750 self._cmd.update_prompt()
751 self._cmd.poutput_json(fcp_dec)
Harald Welte31d2cf02021-04-03 10:47:29 +0200752
Harald Weltec91085e2022-02-10 18:05:45 +0100753 def complete_select(self, text, line, begidx, endidx) -> List[str]:
754 """Command Line tab completion for SELECT"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200755 index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
Harald Weltec91085e2022-02-10 18:05:45 +0100756 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200757
Harald Weltec91085e2022-02-10 18:05:45 +0100758 def get_code(self, code):
759 """Use code either directly or try to get it from external data source"""
760 auto = ('PIN1', 'PIN2', 'PUK1', 'PUK2')
Harald Welte31d2cf02021-04-03 10:47:29 +0200761
Harald Weltec91085e2022-02-10 18:05:45 +0100762 if str(code).upper() not in auto:
763 return sanitize_pin_adm(code)
Harald Welte31d2cf02021-04-03 10:47:29 +0200764
Harald Weltec91085e2022-02-10 18:05:45 +0100765 result = card_key_provider_get_field(
766 str(code), key='ICCID', value=self._cmd.iccid)
767 result = sanitize_pin_adm(result)
768 if result:
769 self._cmd.poutput("found %s '%s' for ICCID '%s'" %
770 (code.upper(), result, self._cmd.iccid))
771 else:
772 self._cmd.poutput("cannot find %s for ICCID '%s'" %
773 (code.upper(), self._cmd.iccid))
774 return result
Harald Welte31d2cf02021-04-03 10:47:29 +0200775
Harald Weltec91085e2022-02-10 18:05:45 +0100776 verify_chv_parser = argparse.ArgumentParser()
777 verify_chv_parser.add_argument(
778 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
779 verify_chv_parser.add_argument(
780 '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 +0200781
Harald Weltec91085e2022-02-10 18:05:45 +0100782 @cmd2.with_argparser(verify_chv_parser)
783 def do_verify_chv(self, opts):
Harald Welte12af7932022-02-15 16:39:08 +0100784 """Verify (authenticate) using specified CHV (PIN) code, which is how the specifications
785 call it if you authenticate yourself using the specified PIN. There usually is at least PIN1 and
786 PIN2."""
Harald Weltec91085e2022-02-10 18:05:45 +0100787 pin = self.get_code(opts.pin_code)
788 (data, sw) = self._cmd.card._scc.verify_chv(opts.pin_nr, h2b(pin))
789 self._cmd.poutput("CHV verification successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200790
Harald Weltec91085e2022-02-10 18:05:45 +0100791 unblock_chv_parser = argparse.ArgumentParser()
792 unblock_chv_parser.add_argument(
793 '--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
794 unblock_chv_parser.add_argument(
795 'puk_code', type=str, help='PUK code digits \"PUK1\" or \"PUK2\" to get PUK code from external data source')
796 unblock_chv_parser.add_argument(
797 '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 +0200798
Harald Weltec91085e2022-02-10 18:05:45 +0100799 @cmd2.with_argparser(unblock_chv_parser)
800 def do_unblock_chv(self, opts):
801 """Unblock PIN code using specified PUK code"""
802 new_pin = self.get_code(opts.new_pin_code)
803 puk = self.get_code(opts.puk_code)
804 (data, sw) = self._cmd.card._scc.unblock_chv(
805 opts.pin_nr, h2b(puk), h2b(new_pin))
806 self._cmd.poutput("CHV unblock successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200807
Harald Weltec91085e2022-02-10 18:05:45 +0100808 change_chv_parser = argparse.ArgumentParser()
809 change_chv_parser.add_argument(
810 '--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
811 change_chv_parser.add_argument(
812 'pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
813 change_chv_parser.add_argument(
814 '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 +0200815
Harald Weltec91085e2022-02-10 18:05:45 +0100816 @cmd2.with_argparser(change_chv_parser)
817 def do_change_chv(self, opts):
818 """Change PIN code to a new PIN code"""
819 new_pin = self.get_code(opts.new_pin_code)
820 pin = self.get_code(opts.pin_code)
821 (data, sw) = self._cmd.card._scc.change_chv(
822 opts.pin_nr, h2b(pin), h2b(new_pin))
823 self._cmd.poutput("CHV change successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200824
Harald Weltec91085e2022-02-10 18:05:45 +0100825 disable_chv_parser = argparse.ArgumentParser()
826 disable_chv_parser.add_argument(
827 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
828 disable_chv_parser.add_argument(
829 '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 +0200830
Harald Weltec91085e2022-02-10 18:05:45 +0100831 @cmd2.with_argparser(disable_chv_parser)
832 def do_disable_chv(self, opts):
833 """Disable PIN code using specified PIN code"""
834 pin = self.get_code(opts.pin_code)
835 (data, sw) = self._cmd.card._scc.disable_chv(opts.pin_nr, h2b(pin))
836 self._cmd.poutput("CHV disable successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200837
Harald Weltec91085e2022-02-10 18:05:45 +0100838 enable_chv_parser = argparse.ArgumentParser()
839 enable_chv_parser.add_argument(
840 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
841 enable_chv_parser.add_argument(
842 '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 +0200843
Harald Weltec91085e2022-02-10 18:05:45 +0100844 @cmd2.with_argparser(enable_chv_parser)
845 def do_enable_chv(self, opts):
846 """Enable PIN code using specified PIN code"""
847 pin = self.get_code(opts.pin_code)
848 (data, sw) = self._cmd.card._scc.enable_chv(opts.pin_nr, h2b(pin))
849 self._cmd.poutput("CHV enable successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200850
Harald Weltec91085e2022-02-10 18:05:45 +0100851 def do_deactivate_file(self, opts):
Harald Welte799c3542022-02-15 15:56:28 +0100852 """Deactivate the currently selected EF"""
Harald Weltec91085e2022-02-10 18:05:45 +0100853 (data, sw) = self._cmd.card._scc.deactivate_file()
Harald Weltea4631612021-04-10 18:17:55 +0200854
Harald Welte799c3542022-02-15 15:56:28 +0100855 activate_file_parser = argparse.ArgumentParser()
856 activate_file_parser.add_argument('NAME', type=str, help='File name or FID of file to activate')
857 @cmd2.with_argparser(activate_file_parser)
Harald Weltec91085e2022-02-10 18:05:45 +0100858 def do_activate_file(self, opts):
Harald Welte12af7932022-02-15 16:39:08 +0100859 """Activate the specified EF. This used to be called REHABILITATE in TS 11.11 for classic
860 SIM. You need to specify the name or FID of the file to activate."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200861 (data, sw) = self._cmd.lchan.activate_file(opts.NAME)
Harald Welte485692b2021-05-25 22:21:44 +0200862
Harald Weltec91085e2022-02-10 18:05:45 +0100863 def complete_activate_file(self, text, line, begidx, endidx) -> List[str]:
864 """Command Line tab completion for ACTIVATE FILE"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200865 index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
Harald Weltec91085e2022-02-10 18:05:45 +0100866 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200867
Harald Weltec91085e2022-02-10 18:05:45 +0100868 open_chan_parser = argparse.ArgumentParser()
869 open_chan_parser.add_argument(
870 'chan_nr', type=int, default=0, help='Channel Number')
Harald Welte703f9332021-04-10 18:39:32 +0200871
Harald Weltec91085e2022-02-10 18:05:45 +0100872 @cmd2.with_argparser(open_chan_parser)
873 def do_open_channel(self, opts):
874 """Open a logical channel."""
875 (data, sw) = self._cmd.card._scc.manage_channel(
876 mode='open', lchan_nr=opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200877
Harald Weltec91085e2022-02-10 18:05:45 +0100878 close_chan_parser = argparse.ArgumentParser()
879 close_chan_parser.add_argument(
880 'chan_nr', type=int, default=0, help='Channel Number')
Harald Welte703f9332021-04-10 18:39:32 +0200881
Harald Weltec91085e2022-02-10 18:05:45 +0100882 @cmd2.with_argparser(close_chan_parser)
883 def do_close_channel(self, opts):
884 """Close a logical channel."""
885 (data, sw) = self._cmd.card._scc.manage_channel(
886 mode='close', lchan_nr=opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200887
Harald Weltec91085e2022-02-10 18:05:45 +0100888 def do_status(self, opts):
889 """Perform the STATUS command."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200890 fcp_dec = self._cmd.lchan.status()
Harald Weltec91085e2022-02-10 18:05:45 +0100891 self._cmd.poutput_json(fcp_dec)
Harald Welte34b05d32021-05-25 22:03:13 +0200892
Harald Weltec91085e2022-02-10 18:05:45 +0100893 suspend_uicc_parser = argparse.ArgumentParser()
894 suspend_uicc_parser.add_argument('--min-duration-secs', type=int, default=60,
895 help='Proposed minimum duration of suspension')
896 suspend_uicc_parser.add_argument('--max-duration-secs', type=int, default=24*60*60,
897 help='Proposed maximum duration of suspension')
Harald Welteec950532021-10-20 13:09:00 +0200898
Harald Weltec91085e2022-02-10 18:05:45 +0100899 # not ISO7816-4 but TS 102 221
900 @cmd2.with_argparser(suspend_uicc_parser)
901 def do_suspend_uicc(self, opts):
902 """Perform the SUSPEND UICC command. Only supported on some UICC."""
903 (duration, token, sw) = self._cmd.card._scc.suspend_uicc(min_len_secs=opts.min_duration_secs,
904 max_len_secs=opts.max_duration_secs)
905 self._cmd.poutput(
906 'Negotiated Duration: %u secs, Token: %s, SW: %s' % (duration, token, sw))
Harald Welteec950532021-10-20 13:09:00 +0200907
Harald Weltecab26c72022-08-06 16:12:30 +0200908class Proact(ProactiveHandler):
909 def receive_fetch(self, pcmd: ProactiveCommand):
910 # print its parsed representation
911 print(pcmd.decoded)
912 # TODO: implement the basics, such as SMS Sending, ...
913
914
Harald Welte703f9332021-04-10 18:39:32 +0200915
Harald Weltef2e761c2021-04-11 11:56:44 +0200916option_parser = argparse.ArgumentParser(prog='pySim-shell', description='interactive SIM card shell',
917 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
Harald Welte28c24312021-04-11 12:19:36 +0200918argparse_add_reader_args(option_parser)
Harald Weltec8ff0262021-04-11 12:06:13 +0200919
920global_group = option_parser.add_argument_group('General Options')
921global_group.add_argument('--script', metavar='PATH', default=None,
Harald Welte28c24312021-04-11 12:19:36 +0200922 help='script with pySim-shell commands to be executed automatically at start-up')
Harald Weltec91085e2022-02-10 18:05:45 +0100923global_group.add_argument('--csv', metavar='FILE',
924 default=None, help='Read card data from CSV file')
Philipp Maier76667642021-09-22 16:53:22 +0200925global_group.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
Harald Weltec91085e2022-02-10 18:05:45 +0100926 help="Use automatic card handling machine")
Harald Weltec8ff0262021-04-11 12:06:13 +0200927
928adm_group = global_group.add_mutually_exclusive_group()
929adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
930 help='ADM PIN used for provisioning (overwrites default)')
931adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM1_HEX', dest='pin_adm_hex', default=None,
932 help='ADM PIN used for provisioning, as hex string (16 characters long)')
Harald Welteb2edd142021-01-08 23:29:35 +0100933
934
935if __name__ == '__main__':
936
Harald Weltec91085e2022-02-10 18:05:45 +0100937 # Parse options
938 opts = option_parser.parse_args()
Harald Welteb2edd142021-01-08 23:29:35 +0100939
Harald Weltec91085e2022-02-10 18:05:45 +0100940 # If a script file is specified, be sure that it actually exists
941 if opts.script:
942 if not os.access(opts.script, os.R_OK):
943 print("Invalid script file!")
944 sys.exit(2)
Philipp Maier13e258d2021-04-08 17:48:49 +0200945
Harald Weltec91085e2022-02-10 18:05:45 +0100946 # Register csv-file as card data provider, either from specified CSV
947 # or from CSV file in home directory
948 csv_default = str(Path.home()) + "/.osmocom/pysim/card_data.csv"
949 if opts.csv:
950 card_key_provider_register(CardKeyProviderCsv(opts.csv))
951 if os.path.isfile(csv_default):
952 card_key_provider_register(CardKeyProviderCsv(csv_default))
Philipp Maier2b11c322021-03-17 12:37:39 +0100953
Harald Weltec91085e2022-02-10 18:05:45 +0100954 # Init card reader driver
Harald Weltecab26c72022-08-06 16:12:30 +0200955 sl = init_reader(opts, proactive_handler = Proact())
Harald Weltec91085e2022-02-10 18:05:45 +0100956 if sl is None:
957 exit(1)
Philipp Maierea95c392021-09-16 13:10:19 +0200958
Harald Weltec91085e2022-02-10 18:05:45 +0100959 # Create command layer
960 scc = SimCardCommands(transport=sl)
Philipp Maierea95c392021-09-16 13:10:19 +0200961
Harald Weltec91085e2022-02-10 18:05:45 +0100962 # Create a card handler (for bulk provisioning)
963 if opts.card_handler_config:
964 ch = CardHandlerAuto(None, opts.card_handler_config)
965 else:
966 ch = CardHandler(sl)
Philipp Maier76667642021-09-22 16:53:22 +0200967
Harald Weltec91085e2022-02-10 18:05:45 +0100968 # Detect and initialize the card in the reader. This may fail when there
969 # is no card in the reader or the card is unresponsive. PysimApp is
970 # able to tolerate and recover from that.
971 try:
972 rs, card = init_card(sl)
973 app = PysimApp(card, rs, sl, ch, opts.script)
974 except:
975 print("Card initialization failed with an exception:")
976 print("---------------------8<---------------------")
977 traceback.print_exc()
978 print("---------------------8<---------------------")
979 print("(you may still try to recover from this manually by using the 'equip' command.)")
980 print(
981 " it should also be noted that some readers may behave strangely when no card")
982 print(" is inserted.)")
983 print("")
Philipp Maier7226c092022-06-01 17:58:38 +0200984 app = PysimApp(card, None, sl, ch, opts.script)
Philipp Maierea95c392021-09-16 13:10:19 +0200985
Harald Weltec91085e2022-02-10 18:05:45 +0100986 # If the user supplies an ADM PIN at via commandline args authenticate
987 # immediately so that the user does not have to use the shell commands
988 pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
989 if pin_adm:
990 if not card:
991 print("Card error, cannot do ADM verification with supplied ADM pin now.")
992 try:
993 card.verify_adm(h2b(pin_adm))
994 except Exception as e:
995 print(e)
Philipp Maier228c98e2021-03-10 20:14:06 +0100996
Harald Weltec91085e2022-02-10 18:05:45 +0100997 app.cmdloop()