blob: 11953eb062cfa5ecdac67295fe3c6febe61bb9d8 [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
31from optparse import OptionParser
Philipp Maier2b11c322021-03-17 12:37:39 +010032from pathlib import Path
Harald Welteb2edd142021-01-08 23:29:35 +010033
34from pySim.ts_51_011 import EF, DF, EF_SST_map, EF_AD_mode_map
35from 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
40from pySim.cards import card_detect, Card
41from pySim.utils import h2b, swap_nibbles, rpad, h2s
Philipp Maier46f09af2021-03-25 20:24:27 +010042from pySim.utils import dec_st, init_reader, sanitize_pin_adm, tabulate_str_list, is_hex
Harald Welteb2edd142021-01-08 23:29:35 +010043from pySim.card_handler import card_handler
44
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
48from pySim.ts_31_102 import ADF_USIM
49from pySim.ts_31_103 import ADF_ISIM
50
Philipp Maier2b11c322021-03-17 12:37:39 +010051from pySim.card_data import CardDataCsv, card_data_register, card_data_get_field
52
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 Welteb2edd142021-01-08 23:29:35 +010057 basic_commands = [Iso7816Commands(), UsimCommands()]
58 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()
74
75 def _onchange_numeric_path(self, param_name, old, new):
76 self.update_prompt()
77
Philipp Maier38c74f62021-03-17 17:19:52 +010078 def _onchange_conserve_write(self, param_name, old, new):
79 self.rs.conserve_write = new
80
Harald Welteb2edd142021-01-08 23:29:35 +010081 def update_prompt(self):
82 path_list = self.rs.selected_file.fully_qualified_path(not self.numeric_path)
83 self.prompt = 'pySIM-shell (%s)> ' % ('/'.join(path_list))
84
85 @cmd2.with_category(CUSTOM_CATEGORY)
86 def do_intro(self, _):
87 """Display the intro banner"""
88 self.poutput(self.intro)
89
90 @cmd2.with_category(CUSTOM_CATEGORY)
91 def do_verify_adm(self, arg):
92 """VERIFY the ADM1 PIN"""
Philipp Maier2b11c322021-03-17 12:37:39 +010093 if arg:
94 # use specified ADM-PIN
95 pin_adm = sanitize_pin_adm(arg)
96 else:
97 # try to find an ADM-PIN if none is specified
98 result = card_data_get_field('ADM1', key='ICCID', value=self.iccid)
99 pin_adm = sanitize_pin_adm(result)
100 if pin_adm:
Philipp Maierb63766b2021-03-26 11:50:21 +0100101 self.poutput("found ADM-PIN '%s' for ICCID '%s'" % (result, self.iccid))
102 else:
103 self.poutput("cannot find ADM-PIN for ICCID '%s'" % (self._cmd.iccid))
104 return
Philipp Maier2b11c322021-03-17 12:37:39 +0100105
106 if pin_adm:
107 self.card.verify_adm(h2b(pin_adm))
108 else:
109 self.poutput("error: cannot authenticate, no adm-pin!")
Harald Welteb2edd142021-01-08 23:29:35 +0100110
Philipp Maier2558aa62021-03-10 16:20:02 +0100111 @cmd2.with_category(CUSTOM_CATEGORY)
112 def do_desc(self, opts):
113 """Display human readable file description for the currently selected file"""
114 desc = self.rs.selected_file.desc
115 if desc:
116 self.poutput(desc)
117 else:
118 self.poutput("no description available")
Harald Welteb2edd142021-01-08 23:29:35 +0100119
120
121@with_default_category('ISO7816 Commands')
122class Iso7816Commands(CommandSet):
123 def __init__(self):
124 super().__init__()
125
126 def do_select(self, opts):
127 """SELECT a File (ADF/DF/EF)"""
Philipp Maierf62866f2021-03-10 17:13:15 +0100128 if len(opts.arg_list) == 0:
129 path_list = self._cmd.rs.selected_file.fully_qualified_path(True)
130 path_list_fid = self._cmd.rs.selected_file.fully_qualified_path(False)
131 self._cmd.poutput("currently selected file: " + '/'.join(path_list) + " (" + '/'.join(path_list_fid) + ")")
132 return
133
Harald Welteb2edd142021-01-08 23:29:35 +0100134 path = opts.arg_list[0]
135 fcp_dec = self._cmd.rs.select(path, self._cmd)
136 self._cmd.update_prompt()
137 self._cmd.poutput(json.dumps(fcp_dec, indent=4))
138
139 def complete_select(self, text, line, begidx, endidx) -> List[str]:
140 """Command Line tab completion for SELECT"""
141 index_dict = { 1: self._cmd.rs.selected_file.get_selectable_names() }
142 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
143
Philipp Maier46f09af2021-03-25 20:24:27 +0100144 def get_code(self, code):
145 """Use code either directly or try to get it from external data source"""
146 auto = ('PIN1', 'PIN2', 'PUK1', 'PUK2')
147
148 if str(code).upper() not in auto:
149 return sanitize_pin_adm(code)
150
151 result = card_data_get_field(str(code), key='ICCID', value=self._cmd.iccid)
152 result = sanitize_pin_adm(result)
153 if result:
154 self._cmd.poutput("found %s '%s' for ICCID '%s'" % (code.upper(), result, self._cmd.iccid))
155 else:
156 self._cmd.poutput("cannot find %s for ICCID '%s'" % (code.upper(), self._cmd.iccid))
157 return result
158
Harald Welteb2edd142021-01-08 23:29:35 +0100159 verify_chv_parser = argparse.ArgumentParser()
Philipp Maier46f09af2021-03-25 20:24:27 +0100160 verify_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
161 verify_chv_parser.add_argument('pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
Harald Welteb2edd142021-01-08 23:29:35 +0100162
163 @cmd2.with_argparser(verify_chv_parser)
164 def do_verify_chv(self, opts):
Philipp Maier46f09af2021-03-25 20:24:27 +0100165 """Verify (authenticate) using specified PIN code"""
166 pin = self.get_code(opts.pin_code)
167 (data, sw) = self._cmd.card._scc.verify_chv(opts.pin_nr, h2b(pin))
168 self._cmd.poutput("CHV verfication successful")
169
170 unblock_chv_parser = argparse.ArgumentParser()
171 unblock_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
172 unblock_chv_parser.add_argument('puk_code', type=str, help='PUK code digits \"PUK1\" or \"PUK2\" to get PUK code from external data source')
173 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')
174
175 @cmd2.with_argparser(unblock_chv_parser)
176 def do_unblock_chv(self, opts):
177 """Unblock PIN code using specified PUK code"""
178 new_pin = self.get_code(opts.new_pin_code)
179 puk = self.get_code(opts.puk_code)
180 (data, sw) = self._cmd.card._scc.unblock_chv(opts.pin_nr, h2b(puk), h2b(new_pin))
181 self._cmd.poutput("CHV unblock successful")
182
183 change_chv_parser = argparse.ArgumentParser()
184 change_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
185 change_chv_parser.add_argument('pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
186 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')
187
188 @cmd2.with_argparser(change_chv_parser)
189 def do_change_chv(self, opts):
190 """Change PIN code to a new PIN code"""
191 new_pin = self.get_code(opts.new_pin_code)
192 pin = self.get_code(opts.pin_code)
193 (data, sw) = self._cmd.card._scc.change_chv(opts.pin_nr, h2b(pin), h2b(new_pin))
194 self._cmd.poutput("CHV change successful")
195
196 disable_chv_parser = argparse.ArgumentParser()
197 disable_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
198 disable_chv_parser.add_argument('pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
199
200 @cmd2.with_argparser(disable_chv_parser)
201 def do_disable_chv(self, opts):
202 """Disable PIN code using specified PIN code"""
203 pin = self.get_code(opts.pin_code)
204 (data, sw) = self._cmd.card._scc.disable_chv(opts.pin_nr, h2b(pin))
205 self._cmd.poutput("CHV disable successful")
206
207 enable_chv_parser = argparse.ArgumentParser()
208 enable_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
209 enable_chv_parser.add_argument('pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
210
211 @cmd2.with_argparser(enable_chv_parser)
212 def do_enable_chv(self, opts):
213 """Enable PIN code using specified PIN code"""
214 pin = self.get_code(opts.pin_code)
215 (data, sw) = self._cmd.card._scc.enable_chv(opts.pin_nr, h2b(pin))
216 self._cmd.poutput("CHV enable successful")
Harald Welteb2edd142021-01-08 23:29:35 +0100217
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)
261 if isinstance(files[f], CardDF):
262 fcp_dec = self._cmd.rs.select(f, self._cmd)
263 self.walk(indent + 1, action, context)
264 fcp_dec = self._cmd.rs.select("..", self._cmd)
265 elif action:
266 action(f, context)
267
268 def do_tree(self, opts):
269 """Display a filesystem-tree with all selectable files"""
270 self.walk()
271
Philipp Maier24f7bd32021-02-25 17:06:18 +0100272 def export(self, filename, context):
273 context['COUNT'] += 1
274 path_list = self._cmd.rs.selected_file.fully_qualified_path(True)
275 path_list_fid = self._cmd.rs.selected_file.fully_qualified_path(False)
276
277 self._cmd.poutput("#" * 80)
278 file_str = '/'.join(path_list) + "/" + str(filename) + " " * 80
279 self._cmd.poutput("# " + file_str[0:77] + "#")
280 self._cmd.poutput("#" * 80)
281
282 self._cmd.poutput("# directory: %s (%s)" % ('/'.join(path_list), '/'.join(path_list_fid)))
283 try:
284 fcp_dec = self._cmd.rs.select(filename, self._cmd)
285 path_list = self._cmd.rs.selected_file.fully_qualified_path(True)
286 path_list_fid = self._cmd.rs.selected_file.fully_qualified_path(False)
287 self._cmd.poutput("# file: %s (%s)" % (path_list[-1], path_list_fid[-1]))
288
289 fd = fcp_dec['file_descriptor']
290 structure = fd['structure']
291 self._cmd.poutput("# structure: %s" % str(structure))
292
293 for f in path_list:
294 self._cmd.poutput("select " + str(f))
295
296 if structure == 'transparent':
297 result = self._cmd.rs.read_binary()
298 self._cmd.poutput("update_binary " + str(result[0]))
299 if structure == 'cyclic' or structure == 'linear_fixed':
300 num_of_rec = fd['num_of_rec']
301 for r in range(1, num_of_rec + 1):
302 result = self._cmd.rs.read_record(r)
303 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
304 fcp_dec = self._cmd.rs.select("..", self._cmd)
305 except Exception as e:
306 bad_file_str = '/'.join(path_list) + "/" + str(filename) + ", " + str(e)
307 self._cmd.poutput("# bad file: %s" % bad_file_str)
308 context['ERR'] += 1
309 context['BAD'].append(bad_file_str)
310
311 self._cmd.poutput("#")
312
313 export_parser = argparse.ArgumentParser()
314 export_parser.add_argument('--filename', type=str, default=None, help='only export specific file')
315
316 @cmd2.with_argparser(export_parser)
317 def do_export(self, opts):
318 """Export files to script that can be imported back later"""
319 context = {'ERR':0, 'COUNT':0, 'BAD':[]}
320 if opts.filename:
321 self.export(opts.filename, context)
322 else:
323 self.walk(0, self.export, context)
324 self._cmd.poutput("# total files visited: %u" % context['COUNT'])
325 self._cmd.poutput("# bad files: %u" % context['ERR'])
326 for b in context['BAD']:
327 self._cmd.poutput("# " + b)
328 if context['ERR']:
329 raise RuntimeError("unable to export %i file(s)" % context['ERR'])
Harald Welteb2edd142021-01-08 23:29:35 +0100330
331
332@with_default_category('USIM Commands')
333class UsimCommands(CommandSet):
334 def __init__(self):
335 super().__init__()
336
337 def do_read_ust(self, _):
338 """Read + Display the EF.UST"""
339 self._cmd.card.select_adf_by_aid(adf="usim")
340 (res, sw) = self._cmd.card.read_ust()
341 self._cmd.poutput(res[0])
342 self._cmd.poutput(res[1])
343
344 def do_read_ehplmn(self, _):
345 """Read EF.EHPLMN"""
346 self._cmd.card.select_adf_by_aid(adf="usim")
347 (res, sw) = self._cmd.card.read_ehplmn()
348 self._cmd.poutput(res)
349
350def parse_options():
351
352 parser = OptionParser(usage="usage: %prog [options]")
353
354 parser.add_option("-d", "--device", dest="device", metavar="DEV",
355 help="Serial Device for SIM access [default: %default]",
356 default="/dev/ttyUSB0",
357 )
358 parser.add_option("-b", "--baud", dest="baudrate", type="int", metavar="BAUD",
359 help="Baudrate used for SIM access [default: %default]",
360 default=9600,
361 )
362 parser.add_option("-p", "--pcsc-device", dest="pcsc_dev", type='int', metavar="PCSC",
363 help="Which PC/SC reader number for SIM access",
364 default=None,
365 )
366 parser.add_option("--modem-device", dest="modem_dev", metavar="DEV",
367 help="Serial port of modem for Generic SIM Access (3GPP TS 27.007)",
368 default=None,
369 )
370 parser.add_option("--modem-baud", dest="modem_baud", type="int", metavar="BAUD",
371 help="Baudrate used for modem's port [default: %default]",
372 default=115200,
373 )
374 parser.add_option("--osmocon", dest="osmocon_sock", metavar="PATH",
375 help="Socket path for Calypso (e.g. Motorola C1XX) based reader (via OsmocomBB)",
376 default=None,
377 )
Philipp Maier681bc7b2021-03-10 19:52:41 +0100378 parser.add_option("--script", dest="script", metavar="PATH",
379 help="script with shell commands to be executed automatically",
380 default=None,
381 )
Harald Welteb2edd142021-01-08 23:29:35 +0100382
Philipp Maier2b11c322021-03-17 12:37:39 +0100383 parser.add_option("--csv", dest="csv", metavar="FILE",
384 help="Read card data from CSV file",
385 default=None,
386 )
387
Harald Welteb2edd142021-01-08 23:29:35 +0100388 parser.add_option("-a", "--pin-adm", dest="pin_adm",
389 help="ADM PIN used for provisioning (overwrites default)",
390 )
391 parser.add_option("-A", "--pin-adm-hex", dest="pin_adm_hex",
392 help="ADM PIN used for provisioning, as hex string (16 characters long",
393 )
394
395 (options, args) = parser.parse_args()
396
397 if args:
398 parser.error("Extraneous arguments")
399
400 return options
401
402
403
404if __name__ == '__main__':
405
406 # Parse options
407 opts = parse_options()
408
409 # Init card reader driver
410 sl = init_reader(opts)
411 if (sl == None):
412 exit(1)
413
414 # Create command layer
415 scc = SimCardCommands(transport=sl)
416
417 sl.wait_for_card();
418
419 card_handler = card_handler(sl)
420
421 card = card_detect("auto", scc)
422 if card is None:
423 print("No card detected!")
424 sys.exit(2)
425
426 profile = CardProfileUICC()
Philipp Maier1e896f32021-03-10 17:02:53 +0100427 profile.add_application(ADF_USIM())
428 profile.add_application(ADF_ISIM())
429
Harald Welteb2edd142021-01-08 23:29:35 +0100430 rs = RuntimeState(card, profile)
431
432 # FIXME: do this dynamically
433 rs.mf.add_file(DF_TELECOM())
434 rs.mf.add_file(DF_GSM())
Harald Welteb2edd142021-01-08 23:29:35 +0100435
Philipp Maier681bc7b2021-03-10 19:52:41 +0100436 app = PysimApp(card, rs, opts.script)
Philipp Maier9c1a4ec2021-03-10 12:38:15 +0100437 rs.select('MF', app)
Philipp Maier228c98e2021-03-10 20:14:06 +0100438
Philipp Maier2b11c322021-03-17 12:37:39 +0100439 # Register csv-file as card data provider, either from specified CSV
440 # or from CSV file in home directory
441 csv_default = str(Path.home()) + "/.osmocom/pysim/card_data.csv"
442 if opts.csv:
443 card_data_register(CardDataCsv(opts.csv))
444 if os.path.isfile(csv_default):
445 card_data_register(CardDataCsv(csv_default))
446
Philipp Maier228c98e2021-03-10 20:14:06 +0100447 # If the user supplies an ADM PIN at via commandline args authenticate
448 # immediatley so that the user does not have to use the shell commands
449 pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
450 if pin_adm:
451 try:
452 card.verify_adm(h2b(pin_adm))
453 except Exception as e:
454 print(e)
455
Harald Welteb2edd142021-01-08 23:29:35 +0100456 app.cmdloop()