blob: 594ad97492f44df8c7f297c2ae375c8b5c6118f6 [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
26from cmd2 import style, fg, bg
27from 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
Harald Welteb2edd142021-01-08 23:29:35 +010033
Robert Falkenberg9d16fbc2021-04-12 11:43:22 +020034from pySim.ts_51_011 import EF, DF, EF_SST_map
Harald Welteb2edd142021-01-08 23:29:35 +010035from pySim.ts_31_102 import EF_UST_map, EF_USIM_ADF_map
36from pySim.ts_31_103 import EF_IST_map, EF_ISIM_ADF_map
37
38from pySim.exceptions import *
39from pySim.commands import SimCardCommands
Harald Welte28c24312021-04-11 12:19:36 +020040from pySim.transport import init_reader, ApduTracer, argparse_add_reader_args
Philipp Maierbb73e512021-05-05 16:14:00 +020041from pySim.cards import card_detect, SimCard
Harald Welte917d98c2021-04-21 11:51:25 +020042from pySim.utils import h2b, swap_nibbles, rpad, b2h, h2s, JsonEncoder, bertlv_parse_one
Philipp Maier80ce71f2021-04-19 21:24:23 +020043from pySim.utils import dec_st, sanitize_pin_adm, tabulate_str_list, is_hex, boxed_heading_str
Philipp Maierb18eed02021-09-17 12:35:58 +020044from pySim.card_handler import CardHandler
Harald Welteb2edd142021-01-08 23:29:35 +010045
Harald Weltef44256c2021-10-14 15:53:39 +020046from pySim.filesystem import CardMF, RuntimeState, CardDF, CardADF, CardModel
Harald Welteb2edd142021-01-08 23:29:35 +010047from pySim.ts_51_011 import CardProfileSIM, DF_TELECOM, DF_GSM
48from pySim.ts_102_221 import CardProfileUICC
Harald Welte5ce35242021-04-02 20:27:05 +020049from pySim.ts_31_102 import CardApplicationUSIM
50from pySim.ts_31_103 import CardApplicationISIM
Harald Welteb2edd142021-01-08 23:29:35 +010051
Harald Welte4c1dca02021-10-14 17:48:25 +020052# we need to import this module so that the SysmocomSJA2 sub-class of
53# CardModel is created, which will add the ATR-based matching and
54# calling of SysmocomSJA2.add_files. See CardModel.apply_matching_models
Harald Weltef44256c2021-10-14 15:53:39 +020055import pySim.sysmocom_sja2
56
Harald Welte4442b3d2021-04-03 09:00:16 +020057from pySim.card_key_provider import CardKeyProviderCsv, card_key_provider_register, card_key_provider_get_field
Philipp Maier2b11c322021-03-17 12:37:39 +010058
Philipp Maierea95c392021-09-16 13:10:19 +020059def init_card(sl):
60 """
61 Detect card in reader and setup card profile and runtime state. This
62 function must be called at least once on startup. The card and runtime
63 state object (rs) is required for all pySim-shell commands.
64 """
65
66 # Wait up to three seconds for a card in reader and try to detect
67 # the card type.
68 print("Waiting for card...")
69 try:
70 sl.wait_for_card(3)
71 except NoCardError:
72 print("No card detected!")
73 return None, None;
74 except:
75 print("Card not readable!")
76 return None, None;
77
78 card = card_detect("auto", scc)
79 if card is None:
80 print("Could not detect card type!")
81 return None, None;
82
83 # Create runtime state with card profile
84 profile = CardProfileUICC()
Philipp Maier57f65ee2021-10-18 14:09:02 +020085 profile.add_application(CardApplicationUSIM())
86 profile.add_application(CardApplicationISIM())
Philipp Maierea95c392021-09-16 13:10:19 +020087 rs = RuntimeState(card, profile)
88
89 # FIXME: do this dynamically
90 rs.mf.add_file(DF_TELECOM())
91 rs.mf.add_file(DF_GSM())
92
Harald Weltef44256c2021-10-14 15:53:39 +020093 CardModel.apply_matching_models(scc, rs)
94
Philipp Maierea95c392021-09-16 13:10:19 +020095 # inform the transport that we can do context-specific SW interpretation
96 sl.set_sw_interpreter(rs)
97
98 return rs, card
Philipp Maier2b11c322021-03-17 12:37:39 +010099
Harald Welteb2edd142021-01-08 23:29:35 +0100100class PysimApp(cmd2.Cmd):
101 CUSTOM_CATEGORY = 'pySim Commands'
Philipp Maier681bc7b2021-03-10 19:52:41 +0100102 def __init__(self, card, rs, script = None):
Harald Welteb2edd142021-01-08 23:29:35 +0100103 super().__init__(persistent_history_file='~/.pysim_shell_history', allow_cli_args=False,
Philipp Maier5d698e52021-09-16 13:18:01 +0200104 use_ipython=True, auto_load_commands=False, startup_script=script)
Harald Welteb2edd142021-01-08 23:29:35 +0100105 self.intro = style('Welcome to pySim-shell!', fg=fg.red)
106 self.default_category = 'pySim-shell built-in commands'
Philipp Maier5d698e52021-09-16 13:18:01 +0200107 self.card = None
108 self.rs = None
Harald Welteb2edd142021-01-08 23:29:35 +0100109 self.py_locals = { 'card': self.card, 'rs' : self.rs }
Harald Welteb2edd142021-01-08 23:29:35 +0100110 self.numeric_path = False
111 self.add_settable(cmd2.Settable('numeric_path', bool, 'Print File IDs instead of names',
112 onchange_cb=self._onchange_numeric_path))
Philipp Maier38c74f62021-03-17 17:19:52 +0100113 self.conserve_write = True
114 self.add_settable(cmd2.Settable('conserve_write', bool, 'Read and compare before write',
115 onchange_cb=self._onchange_conserve_write))
Harald Welte1748b932021-04-06 21:12:25 +0200116 self.json_pretty_print = True
117 self.add_settable(cmd2.Settable('json_pretty_print', bool, 'Pretty-Print JSON output'))
Harald Welte7829d8a2021-04-10 11:28:53 +0200118 self.apdu_trace = False
119 self.add_settable(cmd2.Settable('apdu_trace', bool, 'Trace and display APDUs exchanged with card',
120 onchange_cb=self._onchange_apdu_trace))
Harald Welte1748b932021-04-06 21:12:25 +0200121
Philipp Maier5d698e52021-09-16 13:18:01 +0200122 self.equip(card, rs)
123
124 def equip(self, card, rs):
125 """
126 Equip pySim-shell with the supplied card and runtime state, add (or remove) all required settables and
127 and commands to enable card operations.
128 """
129
130 # Unequip everything from pySim-shell that would not work in unequipped state
131 if self.rs:
132 self.rs.unregister_cmds(self)
133 cmd_set = self.find_commandsets(Iso7816Commands)
134 if cmd_set:
135 self.unregister_command_set(cmd_set[0])
136 cmd_set = self.find_commandsets(PySimCommands)
137 if cmd_set:
138 self.unregister_command_set(cmd_set[0])
139
140 self.card = card
141 self.rs = rs
142
143 # When a card object and a runtime state is present, (re)equip pySim-shell with everything that is
144 # needed to operate on cards.
145 if self.card and self.rs:
146 self._onchange_conserve_write('conserve_write', False, self.conserve_write)
147 self._onchange_apdu_trace('apdu_trace', False, self.apdu_trace)
148 self.register_command_set(Iso7816Commands())
149 self.register_command_set(PySimCommands())
150 self.iccid, sw = self.card.read_iccid()
151 rs.select('MF', self)
152 else:
153 self.poutput("pySim-shell not equipped!")
154
155 self.update_prompt()
156
Harald Welte1748b932021-04-06 21:12:25 +0200157 def poutput_json(self, data, force_no_pretty = False):
Harald Weltec9cdce32021-04-11 10:28:28 +0200158 """like cmd2.poutput() but for a JSON serializable dict."""
Harald Welte1748b932021-04-06 21:12:25 +0200159 if force_no_pretty or self.json_pretty_print == False:
Harald Welte5e749a72021-04-10 17:18:17 +0200160 output = json.dumps(data, cls=JsonEncoder)
Harald Welte1748b932021-04-06 21:12:25 +0200161 else:
Harald Welte5e749a72021-04-10 17:18:17 +0200162 output = json.dumps(data, cls=JsonEncoder, indent=4)
Harald Welte1748b932021-04-06 21:12:25 +0200163 self.poutput(output)
Harald Welteb2edd142021-01-08 23:29:35 +0100164
165 def _onchange_numeric_path(self, param_name, old, new):
166 self.update_prompt()
167
Philipp Maier38c74f62021-03-17 17:19:52 +0100168 def _onchange_conserve_write(self, param_name, old, new):
Philipp Maier5d698e52021-09-16 13:18:01 +0200169 if self.rs:
170 self.rs.conserve_write = new
Philipp Maier38c74f62021-03-17 17:19:52 +0100171
Harald Welte7829d8a2021-04-10 11:28:53 +0200172 def _onchange_apdu_trace(self, param_name, old, new):
Philipp Maier5d698e52021-09-16 13:18:01 +0200173 if self.card:
174 if new == True:
175 self.card._scc._tp.apdu_tracer = self.Cmd2ApduTracer(self)
176 else:
177 self.card._scc._tp.apdu_tracer = None
Harald Welte7829d8a2021-04-10 11:28:53 +0200178
179 class Cmd2ApduTracer(ApduTracer):
180 def __init__(self, cmd2_app):
181 self.cmd2 = app
182
183 def trace_response(self, cmd, sw, resp):
184 self.cmd2.poutput("-> %s %s" % (cmd[:10], cmd[10:]))
185 self.cmd2.poutput("<- %s: %s" % (sw, resp))
186
Harald Welteb2edd142021-01-08 23:29:35 +0100187 def update_prompt(self):
Philipp Maier5d698e52021-09-16 13:18:01 +0200188 if self.rs:
189 path_list = self.rs.selected_file.fully_qualified_path(not self.numeric_path)
190 self.prompt = 'pySIM-shell (%s)> ' % ('/'.join(path_list))
191 else:
192 self.prompt = 'pySIM-shell (no card)> '
Harald Welteb2edd142021-01-08 23:29:35 +0100193
194 @cmd2.with_category(CUSTOM_CATEGORY)
195 def do_intro(self, _):
196 """Display the intro banner"""
197 self.poutput(self.intro)
198
Philipp Maier5d698e52021-09-16 13:18:01 +0200199 @cmd2.with_category(CUSTOM_CATEGORY)
200 def do_equip(self, opts):
201 """Equip pySim-shell with card"""
202 rs, card = init_card(sl);
203 self.equip(card, rs)
204
Philipp Maierb52feed2021-09-22 16:43:13 +0200205 echo_parser = argparse.ArgumentParser()
206 echo_parser.add_argument('string', help="string to echo on the shell")
207
208 @cmd2.with_argparser(echo_parser)
209 @cmd2.with_category(CUSTOM_CATEGORY)
210 def do_echo(self, opts):
211 self.poutput(opts.string)
Harald Welteb2edd142021-01-08 23:29:35 +0100212
Harald Welte31d2cf02021-04-03 10:47:29 +0200213@with_default_category('pySim Commands')
214class PySimCommands(CommandSet):
Harald Welteb2edd142021-01-08 23:29:35 +0100215 def __init__(self):
216 super().__init__()
217
Philipp Maier5d3e2592021-02-22 17:22:16 +0100218 dir_parser = argparse.ArgumentParser()
219 dir_parser.add_argument('--fids', help='Show file identifiers', action='store_true')
220 dir_parser.add_argument('--names', help='Show file names', action='store_true')
221 dir_parser.add_argument('--apps', help='Show applications', action='store_true')
222 dir_parser.add_argument('--all', help='Show all selectable identifiers and names', action='store_true')
223
224 @cmd2.with_argparser(dir_parser)
225 def do_dir(self, opts):
226 """Show a listing of files available in currently selected DF or MF"""
227 if opts.all:
228 flags = []
229 elif opts.fids or opts.names or opts.apps:
230 flags = ['PARENT', 'SELF']
231 if opts.fids:
232 flags += ['FIDS', 'AIDS']
233 if opts.names:
234 flags += ['FNAMES', 'ANAMES']
235 if opts.apps:
236 flags += ['ANAMES', 'AIDS']
237 else:
238 flags = ['PARENT', 'SELF', 'FNAMES', 'ANAMES']
239 selectables = list(self._cmd.rs.selected_file.get_selectable_names(flags = flags))
240 directory_str = tabulate_str_list(selectables, width = 79, hspace = 2, lspace = 1, align_left = True)
241 path_list = self._cmd.rs.selected_file.fully_qualified_path(True)
242 self._cmd.poutput('/'.join(path_list))
243 path_list = self._cmd.rs.selected_file.fully_qualified_path(False)
244 self._cmd.poutput('/'.join(path_list))
245 self._cmd.poutput(directory_str)
246 self._cmd.poutput("%d files" % len(selectables))
Harald Welteb2edd142021-01-08 23:29:35 +0100247
Philipp Maierff9dae22021-02-25 17:03:21 +0100248 def walk(self, indent = 0, action = None, context = None):
249 """Recursively walk through the file system, starting at the currently selected DF"""
250 files = self._cmd.rs.selected_file.get_selectables(flags = ['FNAMES', 'ANAMES'])
251 for f in files:
252 if not action:
253 output_str = " " * indent + str(f) + (" " * 250)
254 output_str = output_str[0:25]
255 if isinstance(files[f], CardADF):
256 output_str += " " + str(files[f].aid)
257 else:
258 output_str += " " + str(files[f].fid)
259 output_str += " " + str(files[f].desc)
260 self._cmd.poutput(output_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200261
Philipp Maierff9dae22021-02-25 17:03:21 +0100262 if isinstance(files[f], CardDF):
Philipp Maierf408a402021-04-09 21:16:12 +0200263 skip_df=False
264 try:
265 fcp_dec = self._cmd.rs.select(f, self._cmd)
266 except Exception as e:
267 skip_df=True
268 df = self._cmd.rs.selected_file
269 df_path_list = df.fully_qualified_path(True)
270 df_skip_reason_str = '/'.join(df_path_list) + "/" + str(f) + ", " + str(e)
271 if context:
272 context['DF_SKIP'] += 1
273 context['DF_SKIP_REASON'].append(df_skip_reason_str)
274
275 # If the DF was skipped, we never have entered the directory
276 # below, so we must not move up.
277 if skip_df == False:
278 self.walk(indent + 1, action, context)
279 fcp_dec = self._cmd.rs.select("..", self._cmd)
280
Philipp Maierff9dae22021-02-25 17:03:21 +0100281 elif action:
Philipp Maierb152a9e2021-04-01 17:13:03 +0200282 df_before_action = self._cmd.rs.selected_file
Philipp Maierff9dae22021-02-25 17:03:21 +0100283 action(f, context)
Philipp Maierb152a9e2021-04-01 17:13:03 +0200284 # When walking through the file system tree the action must not
285 # always restore the currently selected file to the file that
286 # was selected before executing the action() callback.
287 if df_before_action != self._cmd.rs.selected_file:
Harald Weltec9cdce32021-04-11 10:28:28 +0200288 raise RuntimeError("inconsistent walk, %s is currently selected but expecting %s to be selected"
Philipp Maierb152a9e2021-04-01 17:13:03 +0200289 % (str(self._cmd.rs.selected_file), str(df_before_action)))
Philipp Maierff9dae22021-02-25 17:03:21 +0100290
291 def do_tree(self, opts):
292 """Display a filesystem-tree with all selectable files"""
293 self.walk()
294
Philipp Maier24f7bd32021-02-25 17:06:18 +0100295 def export(self, filename, context):
Philipp Maierac34dcc2021-04-01 17:19:05 +0200296 """ Select and export a single file """
Philipp Maier24f7bd32021-02-25 17:06:18 +0100297 context['COUNT'] += 1
Philipp Maierac34dcc2021-04-01 17:19:05 +0200298 df = self._cmd.rs.selected_file
299
300 if not isinstance(df, CardDF):
301 raise RuntimeError("currently selected file %s is not a DF or ADF" % str(df))
302
303 df_path_list = df.fully_qualified_path(True)
304 df_path_list_fid = df.fully_qualified_path(False)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100305
Philipp Maier80ce71f2021-04-19 21:24:23 +0200306 file_str = '/'.join(df_path_list) + "/" + str(filename)
307 self._cmd.poutput(boxed_heading_str(file_str))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100308
Philipp Maierac34dcc2021-04-01 17:19:05 +0200309 self._cmd.poutput("# directory: %s (%s)" % ('/'.join(df_path_list), '/'.join(df_path_list_fid)))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100310 try:
311 fcp_dec = self._cmd.rs.select(filename, self._cmd)
Philipp Maierac34dcc2021-04-01 17:19:05 +0200312 self._cmd.poutput("# file: %s (%s)" % (self._cmd.rs.selected_file.name, self._cmd.rs.selected_file.fid))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100313
314 fd = fcp_dec['file_descriptor']
315 structure = fd['structure']
316 self._cmd.poutput("# structure: %s" % str(structure))
317
Philipp Maierac34dcc2021-04-01 17:19:05 +0200318 for f in df_path_list:
Philipp Maier24f7bd32021-02-25 17:06:18 +0100319 self._cmd.poutput("select " + str(f))
Philipp Maierac34dcc2021-04-01 17:19:05 +0200320 self._cmd.poutput("select " + self._cmd.rs.selected_file.name)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100321
322 if structure == 'transparent':
323 result = self._cmd.rs.read_binary()
324 self._cmd.poutput("update_binary " + str(result[0]))
Harald Welte917d98c2021-04-21 11:51:25 +0200325 elif structure == 'cyclic' or structure == 'linear_fixed':
Philipp Maier24f7bd32021-02-25 17:06:18 +0100326 num_of_rec = fd['num_of_rec']
327 for r in range(1, num_of_rec + 1):
328 result = self._cmd.rs.read_record(r)
329 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
Harald Welte917d98c2021-04-21 11:51:25 +0200330 elif structure == 'ber_tlv':
331 tags = self._cmd.rs.retrieve_tags()
332 for t in tags:
333 result = self._cmd.rs.retrieve_data(t)
Harald Weltec1475302021-05-21 21:47:55 +0200334 (tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
Harald Welte917d98c2021-04-21 11:51:25 +0200335 self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val)))
336 else:
337 raise RuntimeError('Unsupported structure "%s" of file "%s"' % (structure, filename))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100338 except Exception as e:
Philipp Maierac34dcc2021-04-01 17:19:05 +0200339 bad_file_str = '/'.join(df_path_list) + "/" + str(filename) + ", " + str(e)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100340 self._cmd.poutput("# bad file: %s" % bad_file_str)
341 context['ERR'] += 1
342 context['BAD'].append(bad_file_str)
343
Philipp Maierac34dcc2021-04-01 17:19:05 +0200344 # When reading the file is done, make sure the parent file is
345 # selected again. This will be the usual case, however we need
346 # to check before since we must not select the same DF twice
347 if df != self._cmd.rs.selected_file:
348 self._cmd.rs.select(df.fid or df.aid, self._cmd)
349
Philipp Maier24f7bd32021-02-25 17:06:18 +0100350 self._cmd.poutput("#")
351
352 export_parser = argparse.ArgumentParser()
353 export_parser.add_argument('--filename', type=str, default=None, help='only export specific file')
354
355 @cmd2.with_argparser(export_parser)
356 def do_export(self, opts):
357 """Export files to script that can be imported back later"""
Philipp Maierf408a402021-04-09 21:16:12 +0200358 context = {'ERR':0, 'COUNT':0, 'BAD':[], 'DF_SKIP':0, 'DF_SKIP_REASON':[]}
Philipp Maier24f7bd32021-02-25 17:06:18 +0100359 if opts.filename:
360 self.export(opts.filename, context)
361 else:
362 self.walk(0, self.export, context)
Philipp Maier80ce71f2021-04-19 21:24:23 +0200363
364 self._cmd.poutput(boxed_heading_str("Export summary"))
365
Philipp Maier24f7bd32021-02-25 17:06:18 +0100366 self._cmd.poutput("# total files visited: %u" % context['COUNT'])
367 self._cmd.poutput("# bad files: %u" % context['ERR'])
368 for b in context['BAD']:
369 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200370
371 self._cmd.poutput("# skipped dedicated files(s): %u" % context['DF_SKIP'])
372 for b in context['DF_SKIP_REASON']:
373 self._cmd.poutput("# " + b)
374
375 if context['ERR'] and context['DF_SKIP']:
Harald Weltec9cdce32021-04-11 10:28:28 +0200376 raise RuntimeError("unable to export %i elementary file(s) and %i dedicated file(s)" % (context['ERR'], context['DF_SKIP']))
Philipp Maierf408a402021-04-09 21:16:12 +0200377 elif context['ERR']:
Harald Weltec9cdce32021-04-11 10:28:28 +0200378 raise RuntimeError("unable to export %i elementary file(s)" % context['ERR'])
Philipp Maierf408a402021-04-09 21:16:12 +0200379 elif context['DF_SKIP']:
380 raise RuntimeError("unable to export %i dedicated files(s)" % context['ERR'])
Harald Welteb2edd142021-01-08 23:29:35 +0100381
Harald Weltedaf2b392021-05-03 23:17:29 +0200382 def do_reset(self, opts):
383 """Reset the Card."""
384 atr = self._cmd.rs.reset(self._cmd)
385 self._cmd.poutput('Card ATR: %s' % atr)
386 self._cmd.update_prompt()
387
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200388 def do_desc(self, opts):
389 """Display human readable file description for the currently selected file"""
390 desc = self._cmd.rs.selected_file.desc
391 if desc:
392 self._cmd.poutput(desc)
393 else:
394 self._cmd.poutput("no description available")
395
396 def do_verify_adm(self, arg):
397 """VERIFY the ADM1 PIN"""
398 if arg:
399 # use specified ADM-PIN
400 pin_adm = sanitize_pin_adm(arg)
401 else:
402 # try to find an ADM-PIN if none is specified
403 result = card_key_provider_get_field('ADM1', key='ICCID', value=self._cmd.iccid)
404 pin_adm = sanitize_pin_adm(result)
405 if pin_adm:
406 self._cmd.poutput("found ADM-PIN '%s' for ICCID '%s'" % (result, self._cmd.iccid))
407 else:
Philipp Maierf0241452021-09-22 16:35:55 +0200408 raise ValueError("cannot find ADM-PIN for ICCID '%s'" % (self._cmd.iccid))
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200409
410 if pin_adm:
411 self._cmd.card.verify_adm(h2b(pin_adm))
412 else:
Philipp Maierf0241452021-09-22 16:35:55 +0200413 raise ValueError("error: cannot authenticate, no adm-pin!")
Harald Welteb2edd142021-01-08 23:29:35 +0100414
Harald Welte31d2cf02021-04-03 10:47:29 +0200415@with_default_category('ISO7816 Commands')
416class Iso7816Commands(CommandSet):
417 def __init__(self):
418 super().__init__()
419
420 def do_select(self, opts):
421 """SELECT a File (ADF/DF/EF)"""
422 if len(opts.arg_list) == 0:
423 path_list = self._cmd.rs.selected_file.fully_qualified_path(True)
424 path_list_fid = self._cmd.rs.selected_file.fully_qualified_path(False)
425 self._cmd.poutput("currently selected file: " + '/'.join(path_list) + " (" + '/'.join(path_list_fid) + ")")
426 return
427
428 path = opts.arg_list[0]
429 fcp_dec = self._cmd.rs.select(path, self._cmd)
430 self._cmd.update_prompt()
Harald Welteb00e8932021-04-10 17:19:13 +0200431 self._cmd.poutput_json(fcp_dec)
Harald Welte31d2cf02021-04-03 10:47:29 +0200432
433 def complete_select(self, text, line, begidx, endidx) -> List[str]:
434 """Command Line tab completion for SELECT"""
435 index_dict = { 1: self._cmd.rs.selected_file.get_selectable_names() }
436 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
437
438 def get_code(self, code):
439 """Use code either directly or try to get it from external data source"""
440 auto = ('PIN1', 'PIN2', 'PUK1', 'PUK2')
441
442 if str(code).upper() not in auto:
443 return sanitize_pin_adm(code)
444
445 result = card_key_provider_get_field(str(code), key='ICCID', value=self._cmd.iccid)
446 result = sanitize_pin_adm(result)
447 if result:
448 self._cmd.poutput("found %s '%s' for ICCID '%s'" % (code.upper(), result, self._cmd.iccid))
449 else:
450 self._cmd.poutput("cannot find %s for ICCID '%s'" % (code.upper(), self._cmd.iccid))
451 return result
452
453 verify_chv_parser = argparse.ArgumentParser()
454 verify_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
455 verify_chv_parser.add_argument('pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
456
457 @cmd2.with_argparser(verify_chv_parser)
458 def do_verify_chv(self, opts):
459 """Verify (authenticate) using specified PIN code"""
460 pin = self.get_code(opts.pin_code)
461 (data, sw) = self._cmd.card._scc.verify_chv(opts.pin_nr, h2b(pin))
Harald Weltec9cdce32021-04-11 10:28:28 +0200462 self._cmd.poutput("CHV verification successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200463
464 unblock_chv_parser = argparse.ArgumentParser()
465 unblock_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
466 unblock_chv_parser.add_argument('puk_code', type=str, help='PUK code digits \"PUK1\" or \"PUK2\" to get PUK code from external data source')
467 unblock_chv_parser.add_argument('new_pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
468
469 @cmd2.with_argparser(unblock_chv_parser)
470 def do_unblock_chv(self, opts):
471 """Unblock PIN code using specified PUK code"""
472 new_pin = self.get_code(opts.new_pin_code)
473 puk = self.get_code(opts.puk_code)
474 (data, sw) = self._cmd.card._scc.unblock_chv(opts.pin_nr, h2b(puk), h2b(new_pin))
475 self._cmd.poutput("CHV unblock successful")
476
477 change_chv_parser = argparse.ArgumentParser()
478 change_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
479 change_chv_parser.add_argument('pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
480 change_chv_parser.add_argument('new_pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
481
482 @cmd2.with_argparser(change_chv_parser)
483 def do_change_chv(self, opts):
484 """Change PIN code to a new PIN code"""
485 new_pin = self.get_code(opts.new_pin_code)
486 pin = self.get_code(opts.pin_code)
487 (data, sw) = self._cmd.card._scc.change_chv(opts.pin_nr, h2b(pin), h2b(new_pin))
488 self._cmd.poutput("CHV change successful")
489
490 disable_chv_parser = argparse.ArgumentParser()
491 disable_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
492 disable_chv_parser.add_argument('pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
493
494 @cmd2.with_argparser(disable_chv_parser)
495 def do_disable_chv(self, opts):
496 """Disable PIN code using specified PIN code"""
497 pin = self.get_code(opts.pin_code)
498 (data, sw) = self._cmd.card._scc.disable_chv(opts.pin_nr, h2b(pin))
499 self._cmd.poutput("CHV disable successful")
500
501 enable_chv_parser = argparse.ArgumentParser()
502 enable_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
503 enable_chv_parser.add_argument('pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
504
505 @cmd2.with_argparser(enable_chv_parser)
506 def do_enable_chv(self, opts):
507 """Enable PIN code using specified PIN code"""
508 pin = self.get_code(opts.pin_code)
509 (data, sw) = self._cmd.card._scc.enable_chv(opts.pin_nr, h2b(pin))
510 self._cmd.poutput("CHV enable successful")
511
Harald Weltea4631612021-04-10 18:17:55 +0200512 def do_deactivate_file(self, opts):
513 """Deactivate the current EF"""
Harald Welte485692b2021-05-25 22:21:44 +0200514 (data, sw) = self._cmd.card._scc.deactivate_file()
Harald Weltea4631612021-04-10 18:17:55 +0200515
516 def do_activate_file(self, opts):
Harald Welte485692b2021-05-25 22:21:44 +0200517 """Activate the specified EF"""
518 path = opts.arg_list[0]
519 (data, sw) = self._cmd.rs.activate_file(path)
520
521 def complete_activate_file(self, text, line, begidx, endidx) -> List[str]:
522 """Command Line tab completion for ACTIVATE FILE"""
523 index_dict = { 1: self._cmd.rs.selected_file.get_selectable_names() }
524 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200525
Harald Welte703f9332021-04-10 18:39:32 +0200526 open_chan_parser = argparse.ArgumentParser()
527 open_chan_parser.add_argument('chan_nr', type=int, default=0, help='Channel Number')
528
529 @cmd2.with_argparser(open_chan_parser)
530 def do_open_channel(self, opts):
531 """Open a logical channel."""
532 (data, sw) = self._cmd.card._scc.manage_channel(mode='open', lchan_nr=opts.chan_nr)
533
534 close_chan_parser = argparse.ArgumentParser()
535 close_chan_parser.add_argument('chan_nr', type=int, default=0, help='Channel Number')
536
537 @cmd2.with_argparser(close_chan_parser)
538 def do_close_channel(self, opts):
539 """Close a logical channel."""
540 (data, sw) = self._cmd.card._scc.manage_channel(mode='close', lchan_nr=opts.chan_nr)
541
Harald Welte34b05d32021-05-25 22:03:13 +0200542 def do_status(self, opts):
543 """Perform the STATUS command."""
544 fcp_dec = self._cmd.rs.status()
545 self._cmd.poutput_json(fcp_dec)
546
Harald Welte703f9332021-04-10 18:39:32 +0200547
Harald Weltef2e761c2021-04-11 11:56:44 +0200548option_parser = argparse.ArgumentParser(prog='pySim-shell', description='interactive SIM card shell',
549 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
Harald Welte28c24312021-04-11 12:19:36 +0200550argparse_add_reader_args(option_parser)
Harald Weltec8ff0262021-04-11 12:06:13 +0200551
552global_group = option_parser.add_argument_group('General Options')
553global_group.add_argument('--script', metavar='PATH', default=None,
Harald Welte28c24312021-04-11 12:19:36 +0200554 help='script with pySim-shell commands to be executed automatically at start-up')
555global_group.add_argument('--csv', metavar='FILE', default=None, help='Read card data from CSV file')
Harald Weltec8ff0262021-04-11 12:06:13 +0200556
557adm_group = global_group.add_mutually_exclusive_group()
558adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
559 help='ADM PIN used for provisioning (overwrites default)')
560adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM1_HEX', dest='pin_adm_hex', default=None,
561 help='ADM PIN used for provisioning, as hex string (16 characters long)')
Harald Welteb2edd142021-01-08 23:29:35 +0100562
563
564if __name__ == '__main__':
565
566 # Parse options
Harald Weltef2e761c2021-04-11 11:56:44 +0200567 opts = option_parser.parse_args()
Harald Welteb2edd142021-01-08 23:29:35 +0100568
Philipp Maier13e258d2021-04-08 17:48:49 +0200569 # If a script file is specified, be sure that it actually exists
570 if opts.script:
571 if not os.access(opts.script, os.R_OK):
572 print("Invalid script file!")
573 sys.exit(2)
574
Philipp Maier2b11c322021-03-17 12:37:39 +0100575 # Register csv-file as card data provider, either from specified CSV
576 # or from CSV file in home directory
577 csv_default = str(Path.home()) + "/.osmocom/pysim/card_data.csv"
578 if opts.csv:
Harald Welte4442b3d2021-04-03 09:00:16 +0200579 card_key_provider_register(CardKeyProviderCsv(opts.csv))
Philipp Maier2b11c322021-03-17 12:37:39 +0100580 if os.path.isfile(csv_default):
Harald Welte4442b3d2021-04-03 09:00:16 +0200581 card_key_provider_register(CardKeyProviderCsv(csv_default))
Philipp Maier2b11c322021-03-17 12:37:39 +0100582
Philipp Maierea95c392021-09-16 13:10:19 +0200583 # Init card reader driver
584 sl = init_reader(opts)
585 if sl is None:
586 exit(1)
587
588 # Create command layer
589 scc = SimCardCommands(transport=sl)
590
Philipp Maier5d698e52021-09-16 13:18:01 +0200591 # Detect and initialize the card in the reader. This may fail when there
592 # is no card in the reader or the card is unresponsive. PysimApp is
593 # able to tolerate and recover from that.
594 try:
595 rs, card = init_card(sl)
596 app = PysimApp(card, rs, opts.script)
597 except:
598 print("Card initialization failed with an exception:")
599 print("---------------------8<---------------------")
600 traceback.print_exc()
601 print("---------------------8<---------------------")
602 print("(you may still try to recover from this manually by using the 'equip' command.)")
603 print(" it should also be noted that some readers may behave strangely when no card")
604 print(" is inserted.)")
605 print("")
606 app = PysimApp(None, None, opts.script)
Philipp Maierea95c392021-09-16 13:10:19 +0200607
Philipp Maier228c98e2021-03-10 20:14:06 +0100608 # If the user supplies an ADM PIN at via commandline args authenticate
Harald Weltec9cdce32021-04-11 10:28:28 +0200609 # immediately so that the user does not have to use the shell commands
Philipp Maier228c98e2021-03-10 20:14:06 +0100610 pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
611 if pin_adm:
Philipp Maier5d698e52021-09-16 13:18:01 +0200612 if not card:
613 print("Card error, cannot do ADM verification with supplied ADM pin now.")
Philipp Maier228c98e2021-03-10 20:14:06 +0100614 try:
615 card.verify_adm(h2b(pin_adm))
616 except Exception as e:
617 print(e)
618
Harald Welteb2edd142021-01-08 23:29:35 +0100619 app.cmdloop()