blob: 06e14f63b02542fd4dd8ba234d5fc2427dba3b3b [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
23
24import cmd2
25from cmd2 import style, fg, bg
26from cmd2 import CommandSet, with_default_category, with_argparser
27import argparse
28
29import os
30import sys
Philipp Maier2b11c322021-03-17 12:37:39 +010031from pathlib import Path
Harald Welteb2edd142021-01-08 23:29:35 +010032
Robert Falkenberg9d16fbc2021-04-12 11:43:22 +020033from pySim.ts_51_011 import EF, DF, EF_SST_map
Harald Welteb2edd142021-01-08 23:29:35 +010034from pySim.ts_31_102 import EF_UST_map, EF_USIM_ADF_map
35from pySim.ts_31_103 import EF_IST_map, EF_ISIM_ADF_map
36
37from pySim.exceptions import *
38from pySim.commands import SimCardCommands
Harald Welte28c24312021-04-11 12:19:36 +020039from pySim.transport import init_reader, ApduTracer, argparse_add_reader_args
Philipp Maierbb73e512021-05-05 16:14:00 +020040from pySim.cards import card_detect, SimCard
Harald Welte917d98c2021-04-21 11:51:25 +020041from pySim.utils import h2b, swap_nibbles, rpad, b2h, h2s, JsonEncoder, bertlv_parse_one
Philipp Maier80ce71f2021-04-19 21:24:23 +020042from pySim.utils import dec_st, sanitize_pin_adm, tabulate_str_list, is_hex, boxed_heading_str
Philipp Maierb18eed02021-09-17 12:35:58 +020043from pySim.card_handler import CardHandler
Harald Welteb2edd142021-01-08 23:29:35 +010044
Philipp Maierff9dae22021-02-25 17:03:21 +010045from pySim.filesystem import CardMF, RuntimeState, CardDF, CardADF
Harald Welteb2edd142021-01-08 23:29:35 +010046from pySim.ts_51_011 import CardProfileSIM, DF_TELECOM, DF_GSM
47from pySim.ts_102_221 import CardProfileUICC
Harald Welte5ce35242021-04-02 20:27:05 +020048from pySim.ts_31_102 import CardApplicationUSIM
49from pySim.ts_31_103 import CardApplicationISIM
Harald Welteb2edd142021-01-08 23:29:35 +010050
Harald Welte4442b3d2021-04-03 09:00:16 +020051from pySim.card_key_provider import CardKeyProviderCsv, card_key_provider_register, card_key_provider_get_field
Philipp Maier2b11c322021-03-17 12:37:39 +010052
53
Harald Welteb2edd142021-01-08 23:29:35 +010054class PysimApp(cmd2.Cmd):
55 CUSTOM_CATEGORY = 'pySim Commands'
Philipp Maier681bc7b2021-03-10 19:52:41 +010056 def __init__(self, card, rs, script = None):
Harald Welte31d2cf02021-04-03 10:47:29 +020057 basic_commands = [Iso7816Commands(), PySimCommands()]
Harald Welteb2edd142021-01-08 23:29:35 +010058 super().__init__(persistent_history_file='~/.pysim_shell_history', allow_cli_args=False,
Philipp Maier681bc7b2021-03-10 19:52:41 +010059 use_ipython=True, auto_load_commands=False, command_sets=basic_commands, startup_script=script)
Harald Welteb2edd142021-01-08 23:29:35 +010060 self.intro = style('Welcome to pySim-shell!', fg=fg.red)
61 self.default_category = 'pySim-shell built-in commands'
62 self.card = card
Philipp Maier2b11c322021-03-17 12:37:39 +010063 iccid, sw = self.card.read_iccid()
64 self.iccid = iccid
Harald Welteb2edd142021-01-08 23:29:35 +010065 self.rs = rs
66 self.py_locals = { 'card': self.card, 'rs' : self.rs }
Harald Welteb2edd142021-01-08 23:29:35 +010067 self.numeric_path = False
68 self.add_settable(cmd2.Settable('numeric_path', bool, 'Print File IDs instead of names',
69 onchange_cb=self._onchange_numeric_path))
Philipp Maier38c74f62021-03-17 17:19:52 +010070 self.conserve_write = True
71 self.add_settable(cmd2.Settable('conserve_write', bool, 'Read and compare before write',
72 onchange_cb=self._onchange_conserve_write))
Harald Welteb2edd142021-01-08 23:29:35 +010073 self.update_prompt()
Harald Welte1748b932021-04-06 21:12:25 +020074 self.json_pretty_print = True
75 self.add_settable(cmd2.Settable('json_pretty_print', bool, 'Pretty-Print JSON output'))
Harald Welte7829d8a2021-04-10 11:28:53 +020076 self.apdu_trace = False
77 self.add_settable(cmd2.Settable('apdu_trace', bool, 'Trace and display APDUs exchanged with card',
78 onchange_cb=self._onchange_apdu_trace))
Harald Welte1748b932021-04-06 21:12:25 +020079
80 def poutput_json(self, data, force_no_pretty = False):
Harald Weltec9cdce32021-04-11 10:28:28 +020081 """like cmd2.poutput() but for a JSON serializable dict."""
Harald Welte1748b932021-04-06 21:12:25 +020082 if force_no_pretty or self.json_pretty_print == False:
Harald Welte5e749a72021-04-10 17:18:17 +020083 output = json.dumps(data, cls=JsonEncoder)
Harald Welte1748b932021-04-06 21:12:25 +020084 else:
Harald Welte5e749a72021-04-10 17:18:17 +020085 output = json.dumps(data, cls=JsonEncoder, indent=4)
Harald Welte1748b932021-04-06 21:12:25 +020086 self.poutput(output)
Harald Welteb2edd142021-01-08 23:29:35 +010087
88 def _onchange_numeric_path(self, param_name, old, new):
89 self.update_prompt()
90
Philipp Maier38c74f62021-03-17 17:19:52 +010091 def _onchange_conserve_write(self, param_name, old, new):
92 self.rs.conserve_write = new
93
Harald Welte7829d8a2021-04-10 11:28:53 +020094 def _onchange_apdu_trace(self, param_name, old, new):
95 if new == True:
96 self.card._scc._tp.apdu_tracer = self.Cmd2ApduTracer(self)
97 else:
98 self.card._scc._tp.apdu_tracer = None
99
100 class Cmd2ApduTracer(ApduTracer):
101 def __init__(self, cmd2_app):
102 self.cmd2 = app
103
104 def trace_response(self, cmd, sw, resp):
105 self.cmd2.poutput("-> %s %s" % (cmd[:10], cmd[10:]))
106 self.cmd2.poutput("<- %s: %s" % (sw, resp))
107
Harald Welteb2edd142021-01-08 23:29:35 +0100108 def update_prompt(self):
109 path_list = self.rs.selected_file.fully_qualified_path(not self.numeric_path)
110 self.prompt = 'pySIM-shell (%s)> ' % ('/'.join(path_list))
111
112 @cmd2.with_category(CUSTOM_CATEGORY)
113 def do_intro(self, _):
114 """Display the intro banner"""
115 self.poutput(self.intro)
116
Harald Welteb2edd142021-01-08 23:29:35 +0100117
Harald Welte31d2cf02021-04-03 10:47:29 +0200118@with_default_category('pySim Commands')
119class PySimCommands(CommandSet):
Harald Welteb2edd142021-01-08 23:29:35 +0100120 def __init__(self):
121 super().__init__()
122
Philipp Maier5d3e2592021-02-22 17:22:16 +0100123 dir_parser = argparse.ArgumentParser()
124 dir_parser.add_argument('--fids', help='Show file identifiers', action='store_true')
125 dir_parser.add_argument('--names', help='Show file names', action='store_true')
126 dir_parser.add_argument('--apps', help='Show applications', action='store_true')
127 dir_parser.add_argument('--all', help='Show all selectable identifiers and names', action='store_true')
128
129 @cmd2.with_argparser(dir_parser)
130 def do_dir(self, opts):
131 """Show a listing of files available in currently selected DF or MF"""
132 if opts.all:
133 flags = []
134 elif opts.fids or opts.names or opts.apps:
135 flags = ['PARENT', 'SELF']
136 if opts.fids:
137 flags += ['FIDS', 'AIDS']
138 if opts.names:
139 flags += ['FNAMES', 'ANAMES']
140 if opts.apps:
141 flags += ['ANAMES', 'AIDS']
142 else:
143 flags = ['PARENT', 'SELF', 'FNAMES', 'ANAMES']
144 selectables = list(self._cmd.rs.selected_file.get_selectable_names(flags = flags))
145 directory_str = tabulate_str_list(selectables, width = 79, hspace = 2, lspace = 1, align_left = True)
146 path_list = self._cmd.rs.selected_file.fully_qualified_path(True)
147 self._cmd.poutput('/'.join(path_list))
148 path_list = self._cmd.rs.selected_file.fully_qualified_path(False)
149 self._cmd.poutput('/'.join(path_list))
150 self._cmd.poutput(directory_str)
151 self._cmd.poutput("%d files" % len(selectables))
Harald Welteb2edd142021-01-08 23:29:35 +0100152
Philipp Maierff9dae22021-02-25 17:03:21 +0100153 def walk(self, indent = 0, action = None, context = None):
154 """Recursively walk through the file system, starting at the currently selected DF"""
155 files = self._cmd.rs.selected_file.get_selectables(flags = ['FNAMES', 'ANAMES'])
156 for f in files:
157 if not action:
158 output_str = " " * indent + str(f) + (" " * 250)
159 output_str = output_str[0:25]
160 if isinstance(files[f], CardADF):
161 output_str += " " + str(files[f].aid)
162 else:
163 output_str += " " + str(files[f].fid)
164 output_str += " " + str(files[f].desc)
165 self._cmd.poutput(output_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200166
Philipp Maierff9dae22021-02-25 17:03:21 +0100167 if isinstance(files[f], CardDF):
Philipp Maierf408a402021-04-09 21:16:12 +0200168 skip_df=False
169 try:
170 fcp_dec = self._cmd.rs.select(f, self._cmd)
171 except Exception as e:
172 skip_df=True
173 df = self._cmd.rs.selected_file
174 df_path_list = df.fully_qualified_path(True)
175 df_skip_reason_str = '/'.join(df_path_list) + "/" + str(f) + ", " + str(e)
176 if context:
177 context['DF_SKIP'] += 1
178 context['DF_SKIP_REASON'].append(df_skip_reason_str)
179
180 # If the DF was skipped, we never have entered the directory
181 # below, so we must not move up.
182 if skip_df == False:
183 self.walk(indent + 1, action, context)
184 fcp_dec = self._cmd.rs.select("..", self._cmd)
185
Philipp Maierff9dae22021-02-25 17:03:21 +0100186 elif action:
Philipp Maierb152a9e2021-04-01 17:13:03 +0200187 df_before_action = self._cmd.rs.selected_file
Philipp Maierff9dae22021-02-25 17:03:21 +0100188 action(f, context)
Philipp Maierb152a9e2021-04-01 17:13:03 +0200189 # When walking through the file system tree the action must not
190 # always restore the currently selected file to the file that
191 # was selected before executing the action() callback.
192 if df_before_action != self._cmd.rs.selected_file:
Harald Weltec9cdce32021-04-11 10:28:28 +0200193 raise RuntimeError("inconsistent walk, %s is currently selected but expecting %s to be selected"
Philipp Maierb152a9e2021-04-01 17:13:03 +0200194 % (str(self._cmd.rs.selected_file), str(df_before_action)))
Philipp Maierff9dae22021-02-25 17:03:21 +0100195
196 def do_tree(self, opts):
197 """Display a filesystem-tree with all selectable files"""
198 self.walk()
199
Philipp Maier24f7bd32021-02-25 17:06:18 +0100200 def export(self, filename, context):
Philipp Maierac34dcc2021-04-01 17:19:05 +0200201 """ Select and export a single file """
Philipp Maier24f7bd32021-02-25 17:06:18 +0100202 context['COUNT'] += 1
Philipp Maierac34dcc2021-04-01 17:19:05 +0200203 df = self._cmd.rs.selected_file
204
205 if not isinstance(df, CardDF):
206 raise RuntimeError("currently selected file %s is not a DF or ADF" % str(df))
207
208 df_path_list = df.fully_qualified_path(True)
209 df_path_list_fid = df.fully_qualified_path(False)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100210
Philipp Maier80ce71f2021-04-19 21:24:23 +0200211 file_str = '/'.join(df_path_list) + "/" + str(filename)
212 self._cmd.poutput(boxed_heading_str(file_str))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100213
Philipp Maierac34dcc2021-04-01 17:19:05 +0200214 self._cmd.poutput("# directory: %s (%s)" % ('/'.join(df_path_list), '/'.join(df_path_list_fid)))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100215 try:
216 fcp_dec = self._cmd.rs.select(filename, self._cmd)
Philipp Maierac34dcc2021-04-01 17:19:05 +0200217 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 +0100218
219 fd = fcp_dec['file_descriptor']
220 structure = fd['structure']
221 self._cmd.poutput("# structure: %s" % str(structure))
222
Philipp Maierac34dcc2021-04-01 17:19:05 +0200223 for f in df_path_list:
Philipp Maier24f7bd32021-02-25 17:06:18 +0100224 self._cmd.poutput("select " + str(f))
Philipp Maierac34dcc2021-04-01 17:19:05 +0200225 self._cmd.poutput("select " + self._cmd.rs.selected_file.name)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100226
227 if structure == 'transparent':
228 result = self._cmd.rs.read_binary()
229 self._cmd.poutput("update_binary " + str(result[0]))
Harald Welte917d98c2021-04-21 11:51:25 +0200230 elif structure == 'cyclic' or structure == 'linear_fixed':
Philipp Maier24f7bd32021-02-25 17:06:18 +0100231 num_of_rec = fd['num_of_rec']
232 for r in range(1, num_of_rec + 1):
233 result = self._cmd.rs.read_record(r)
234 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
Harald Welte917d98c2021-04-21 11:51:25 +0200235 elif structure == 'ber_tlv':
236 tags = self._cmd.rs.retrieve_tags()
237 for t in tags:
238 result = self._cmd.rs.retrieve_data(t)
Harald Weltec1475302021-05-21 21:47:55 +0200239 (tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
Harald Welte917d98c2021-04-21 11:51:25 +0200240 self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val)))
241 else:
242 raise RuntimeError('Unsupported structure "%s" of file "%s"' % (structure, filename))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100243 except Exception as e:
Philipp Maierac34dcc2021-04-01 17:19:05 +0200244 bad_file_str = '/'.join(df_path_list) + "/" + str(filename) + ", " + str(e)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100245 self._cmd.poutput("# bad file: %s" % bad_file_str)
246 context['ERR'] += 1
247 context['BAD'].append(bad_file_str)
248
Philipp Maierac34dcc2021-04-01 17:19:05 +0200249 # When reading the file is done, make sure the parent file is
250 # selected again. This will be the usual case, however we need
251 # to check before since we must not select the same DF twice
252 if df != self._cmd.rs.selected_file:
253 self._cmd.rs.select(df.fid or df.aid, self._cmd)
254
Philipp Maier24f7bd32021-02-25 17:06:18 +0100255 self._cmd.poutput("#")
256
257 export_parser = argparse.ArgumentParser()
258 export_parser.add_argument('--filename', type=str, default=None, help='only export specific file')
259
260 @cmd2.with_argparser(export_parser)
261 def do_export(self, opts):
262 """Export files to script that can be imported back later"""
Philipp Maierf408a402021-04-09 21:16:12 +0200263 context = {'ERR':0, 'COUNT':0, 'BAD':[], 'DF_SKIP':0, 'DF_SKIP_REASON':[]}
Philipp Maier24f7bd32021-02-25 17:06:18 +0100264 if opts.filename:
265 self.export(opts.filename, context)
266 else:
267 self.walk(0, self.export, context)
Philipp Maier80ce71f2021-04-19 21:24:23 +0200268
269 self._cmd.poutput(boxed_heading_str("Export summary"))
270
Philipp Maier24f7bd32021-02-25 17:06:18 +0100271 self._cmd.poutput("# total files visited: %u" % context['COUNT'])
272 self._cmd.poutput("# bad files: %u" % context['ERR'])
273 for b in context['BAD']:
274 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200275
276 self._cmd.poutput("# skipped dedicated files(s): %u" % context['DF_SKIP'])
277 for b in context['DF_SKIP_REASON']:
278 self._cmd.poutput("# " + b)
279
280 if context['ERR'] and context['DF_SKIP']:
Harald Weltec9cdce32021-04-11 10:28:28 +0200281 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 +0200282 elif context['ERR']:
Harald Weltec9cdce32021-04-11 10:28:28 +0200283 raise RuntimeError("unable to export %i elementary file(s)" % context['ERR'])
Philipp Maierf408a402021-04-09 21:16:12 +0200284 elif context['DF_SKIP']:
285 raise RuntimeError("unable to export %i dedicated files(s)" % context['ERR'])
Harald Welteb2edd142021-01-08 23:29:35 +0100286
Harald Weltedaf2b392021-05-03 23:17:29 +0200287 def do_reset(self, opts):
288 """Reset the Card."""
289 atr = self._cmd.rs.reset(self._cmd)
290 self._cmd.poutput('Card ATR: %s' % atr)
291 self._cmd.update_prompt()
292
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200293 def do_desc(self, opts):
294 """Display human readable file description for the currently selected file"""
295 desc = self._cmd.rs.selected_file.desc
296 if desc:
297 self._cmd.poutput(desc)
298 else:
299 self._cmd.poutput("no description available")
300
301 def do_verify_adm(self, arg):
302 """VERIFY the ADM1 PIN"""
303 if arg:
304 # use specified ADM-PIN
305 pin_adm = sanitize_pin_adm(arg)
306 else:
307 # try to find an ADM-PIN if none is specified
308 result = card_key_provider_get_field('ADM1', key='ICCID', value=self._cmd.iccid)
309 pin_adm = sanitize_pin_adm(result)
310 if pin_adm:
311 self._cmd.poutput("found ADM-PIN '%s' for ICCID '%s'" % (result, self._cmd.iccid))
312 else:
313 self._cmd.poutput("cannot find ADM-PIN for ICCID '%s'" % (self._cmd.iccid))
314 return
315
316 if pin_adm:
317 self._cmd.card.verify_adm(h2b(pin_adm))
318 else:
319 self._cmd.poutput("error: cannot authenticate, no adm-pin!")
Harald Welteb2edd142021-01-08 23:29:35 +0100320
Harald Welte31d2cf02021-04-03 10:47:29 +0200321@with_default_category('ISO7816 Commands')
322class Iso7816Commands(CommandSet):
323 def __init__(self):
324 super().__init__()
325
326 def do_select(self, opts):
327 """SELECT a File (ADF/DF/EF)"""
328 if len(opts.arg_list) == 0:
329 path_list = self._cmd.rs.selected_file.fully_qualified_path(True)
330 path_list_fid = self._cmd.rs.selected_file.fully_qualified_path(False)
331 self._cmd.poutput("currently selected file: " + '/'.join(path_list) + " (" + '/'.join(path_list_fid) + ")")
332 return
333
334 path = opts.arg_list[0]
335 fcp_dec = self._cmd.rs.select(path, self._cmd)
336 self._cmd.update_prompt()
Harald Welteb00e8932021-04-10 17:19:13 +0200337 self._cmd.poutput_json(fcp_dec)
Harald Welte31d2cf02021-04-03 10:47:29 +0200338
339 def complete_select(self, text, line, begidx, endidx) -> List[str]:
340 """Command Line tab completion for SELECT"""
341 index_dict = { 1: self._cmd.rs.selected_file.get_selectable_names() }
342 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
343
344 def get_code(self, code):
345 """Use code either directly or try to get it from external data source"""
346 auto = ('PIN1', 'PIN2', 'PUK1', 'PUK2')
347
348 if str(code).upper() not in auto:
349 return sanitize_pin_adm(code)
350
351 result = card_key_provider_get_field(str(code), key='ICCID', value=self._cmd.iccid)
352 result = sanitize_pin_adm(result)
353 if result:
354 self._cmd.poutput("found %s '%s' for ICCID '%s'" % (code.upper(), result, self._cmd.iccid))
355 else:
356 self._cmd.poutput("cannot find %s for ICCID '%s'" % (code.upper(), self._cmd.iccid))
357 return result
358
359 verify_chv_parser = argparse.ArgumentParser()
360 verify_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
361 verify_chv_parser.add_argument('pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
362
363 @cmd2.with_argparser(verify_chv_parser)
364 def do_verify_chv(self, opts):
365 """Verify (authenticate) using specified PIN code"""
366 pin = self.get_code(opts.pin_code)
367 (data, sw) = self._cmd.card._scc.verify_chv(opts.pin_nr, h2b(pin))
Harald Weltec9cdce32021-04-11 10:28:28 +0200368 self._cmd.poutput("CHV verification successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200369
370 unblock_chv_parser = argparse.ArgumentParser()
371 unblock_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
372 unblock_chv_parser.add_argument('puk_code', type=str, help='PUK code digits \"PUK1\" or \"PUK2\" to get PUK code from external data source')
373 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')
374
375 @cmd2.with_argparser(unblock_chv_parser)
376 def do_unblock_chv(self, opts):
377 """Unblock PIN code using specified PUK code"""
378 new_pin = self.get_code(opts.new_pin_code)
379 puk = self.get_code(opts.puk_code)
380 (data, sw) = self._cmd.card._scc.unblock_chv(opts.pin_nr, h2b(puk), h2b(new_pin))
381 self._cmd.poutput("CHV unblock successful")
382
383 change_chv_parser = argparse.ArgumentParser()
384 change_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
385 change_chv_parser.add_argument('pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
386 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')
387
388 @cmd2.with_argparser(change_chv_parser)
389 def do_change_chv(self, opts):
390 """Change PIN code to a new PIN code"""
391 new_pin = self.get_code(opts.new_pin_code)
392 pin = self.get_code(opts.pin_code)
393 (data, sw) = self._cmd.card._scc.change_chv(opts.pin_nr, h2b(pin), h2b(new_pin))
394 self._cmd.poutput("CHV change successful")
395
396 disable_chv_parser = argparse.ArgumentParser()
397 disable_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
398 disable_chv_parser.add_argument('pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
399
400 @cmd2.with_argparser(disable_chv_parser)
401 def do_disable_chv(self, opts):
402 """Disable PIN code using specified PIN code"""
403 pin = self.get_code(opts.pin_code)
404 (data, sw) = self._cmd.card._scc.disable_chv(opts.pin_nr, h2b(pin))
405 self._cmd.poutput("CHV disable successful")
406
407 enable_chv_parser = argparse.ArgumentParser()
408 enable_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
409 enable_chv_parser.add_argument('pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
410
411 @cmd2.with_argparser(enable_chv_parser)
412 def do_enable_chv(self, opts):
413 """Enable PIN code using specified PIN code"""
414 pin = self.get_code(opts.pin_code)
415 (data, sw) = self._cmd.card._scc.enable_chv(opts.pin_nr, h2b(pin))
416 self._cmd.poutput("CHV enable successful")
417
Harald Weltea4631612021-04-10 18:17:55 +0200418 def do_deactivate_file(self, opts):
419 """Deactivate the current EF"""
Harald Welte485692b2021-05-25 22:21:44 +0200420 (data, sw) = self._cmd.card._scc.deactivate_file()
Harald Weltea4631612021-04-10 18:17:55 +0200421
422 def do_activate_file(self, opts):
Harald Welte485692b2021-05-25 22:21:44 +0200423 """Activate the specified EF"""
424 path = opts.arg_list[0]
425 (data, sw) = self._cmd.rs.activate_file(path)
426
427 def complete_activate_file(self, text, line, begidx, endidx) -> List[str]:
428 """Command Line tab completion for ACTIVATE FILE"""
429 index_dict = { 1: self._cmd.rs.selected_file.get_selectable_names() }
430 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200431
Harald Welte703f9332021-04-10 18:39:32 +0200432 open_chan_parser = argparse.ArgumentParser()
433 open_chan_parser.add_argument('chan_nr', type=int, default=0, help='Channel Number')
434
435 @cmd2.with_argparser(open_chan_parser)
436 def do_open_channel(self, opts):
437 """Open a logical channel."""
438 (data, sw) = self._cmd.card._scc.manage_channel(mode='open', lchan_nr=opts.chan_nr)
439
440 close_chan_parser = argparse.ArgumentParser()
441 close_chan_parser.add_argument('chan_nr', type=int, default=0, help='Channel Number')
442
443 @cmd2.with_argparser(close_chan_parser)
444 def do_close_channel(self, opts):
445 """Close a logical channel."""
446 (data, sw) = self._cmd.card._scc.manage_channel(mode='close', lchan_nr=opts.chan_nr)
447
Harald Welte34b05d32021-05-25 22:03:13 +0200448 def do_status(self, opts):
449 """Perform the STATUS command."""
450 fcp_dec = self._cmd.rs.status()
451 self._cmd.poutput_json(fcp_dec)
452
Harald Welte703f9332021-04-10 18:39:32 +0200453
Harald Weltef2e761c2021-04-11 11:56:44 +0200454option_parser = argparse.ArgumentParser(prog='pySim-shell', description='interactive SIM card shell',
455 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
Harald Welte28c24312021-04-11 12:19:36 +0200456argparse_add_reader_args(option_parser)
Harald Weltec8ff0262021-04-11 12:06:13 +0200457
458global_group = option_parser.add_argument_group('General Options')
459global_group.add_argument('--script', metavar='PATH', default=None,
Harald Welte28c24312021-04-11 12:19:36 +0200460 help='script with pySim-shell commands to be executed automatically at start-up')
461global_group.add_argument('--csv', metavar='FILE', default=None, help='Read card data from CSV file')
Harald Weltec8ff0262021-04-11 12:06:13 +0200462
463adm_group = global_group.add_mutually_exclusive_group()
464adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
465 help='ADM PIN used for provisioning (overwrites default)')
466adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM1_HEX', dest='pin_adm_hex', default=None,
467 help='ADM PIN used for provisioning, as hex string (16 characters long)')
Harald Welteb2edd142021-01-08 23:29:35 +0100468
469
470if __name__ == '__main__':
471
472 # Parse options
Harald Weltef2e761c2021-04-11 11:56:44 +0200473 opts = option_parser.parse_args()
Harald Welteb2edd142021-01-08 23:29:35 +0100474
475 # Init card reader driver
476 sl = init_reader(opts)
477 if (sl == None):
478 exit(1)
479
480 # Create command layer
481 scc = SimCardCommands(transport=sl)
482
483 sl.wait_for_card();
484
Philipp Maierb18eed02021-09-17 12:35:58 +0200485 card_handler = CardHandler(sl)
Harald Welteb2edd142021-01-08 23:29:35 +0100486
487 card = card_detect("auto", scc)
488 if card is None:
489 print("No card detected!")
490 sys.exit(2)
491
492 profile = CardProfileUICC()
Harald Welte5ce35242021-04-02 20:27:05 +0200493 profile.add_application(CardApplicationUSIM)
494 profile.add_application(CardApplicationISIM)
Philipp Maier1e896f32021-03-10 17:02:53 +0100495
Harald Welteb2edd142021-01-08 23:29:35 +0100496 rs = RuntimeState(card, profile)
Harald Welte4f2c5462021-04-03 11:48:22 +0200497 # inform the transport that we can do context-specific SW interpretation
498 sl.set_sw_interpreter(rs)
Harald Welteb2edd142021-01-08 23:29:35 +0100499
500 # FIXME: do this dynamically
501 rs.mf.add_file(DF_TELECOM())
502 rs.mf.add_file(DF_GSM())
Harald Welteb2edd142021-01-08 23:29:35 +0100503
Philipp Maier13e258d2021-04-08 17:48:49 +0200504 # If a script file is specified, be sure that it actually exists
505 if opts.script:
506 if not os.access(opts.script, os.R_OK):
507 print("Invalid script file!")
508 sys.exit(2)
509
Philipp Maier681bc7b2021-03-10 19:52:41 +0100510 app = PysimApp(card, rs, opts.script)
Philipp Maier9c1a4ec2021-03-10 12:38:15 +0100511 rs.select('MF', app)
Philipp Maier228c98e2021-03-10 20:14:06 +0100512
Philipp Maier2b11c322021-03-17 12:37:39 +0100513 # Register csv-file as card data provider, either from specified CSV
514 # or from CSV file in home directory
515 csv_default = str(Path.home()) + "/.osmocom/pysim/card_data.csv"
516 if opts.csv:
Harald Welte4442b3d2021-04-03 09:00:16 +0200517 card_key_provider_register(CardKeyProviderCsv(opts.csv))
Philipp Maier2b11c322021-03-17 12:37:39 +0100518 if os.path.isfile(csv_default):
Harald Welte4442b3d2021-04-03 09:00:16 +0200519 card_key_provider_register(CardKeyProviderCsv(csv_default))
Philipp Maier2b11c322021-03-17 12:37:39 +0100520
Philipp Maier228c98e2021-03-10 20:14:06 +0100521 # If the user supplies an ADM PIN at via commandline args authenticate
Harald Weltec9cdce32021-04-11 10:28:28 +0200522 # immediately so that the user does not have to use the shell commands
Philipp Maier228c98e2021-03-10 20:14:06 +0100523 pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
524 if pin_adm:
525 try:
526 card.verify_adm(h2b(pin_adm))
527 except Exception as e:
528 print(e)
529
Harald Welteb2edd142021-01-08 23:29:35 +0100530 app.cmdloop()