2021-01-08 22:29:35 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
# Interactive shell for working with SIM / UICC / USIM / ISIM cards
|
|
|
|
#
|
2022-08-06 14:12:30 +00:00
|
|
|
# (C) 2021-2022 by Harald Welte <laforge@osmocom.org>
|
2021-01-08 22:29:35 +00:00
|
|
|
#
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 2 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
from typing import List
|
|
|
|
|
|
|
|
import json
|
2021-09-16 11:18:01 +00:00
|
|
|
import traceback
|
2021-01-08 22:29:35 +00:00
|
|
|
|
|
|
|
import cmd2
|
2022-07-14 12:08:24 +00:00
|
|
|
from cmd2 import style, fg
|
2021-01-08 22:29:35 +00:00
|
|
|
from cmd2 import CommandSet, with_default_category, with_argparser
|
|
|
|
import argparse
|
|
|
|
|
|
|
|
import os
|
|
|
|
import sys
|
2021-03-17 11:37:39 +00:00
|
|
|
from pathlib import Path
|
2021-09-22 14:53:22 +00:00
|
|
|
from io import StringIO
|
2021-01-08 22:29:35 +00:00
|
|
|
|
2022-08-06 14:12:30 +00:00
|
|
|
from pprint import pprint as pp
|
|
|
|
|
2021-01-08 22:29:35 +00:00
|
|
|
from pySim.exceptions import *
|
|
|
|
from pySim.commands import SimCardCommands
|
2022-08-06 14:12:30 +00:00
|
|
|
from pySim.transport import init_reader, ApduTracer, argparse_add_reader_args, ProactiveHandler
|
2021-05-05 14:14:00 +00:00
|
|
|
from pySim.cards import card_detect, SimCard
|
2022-07-16 09:53:21 +00:00
|
|
|
from pySim.utils import h2b, swap_nibbles, rpad, b2h, JsonEncoder, bertlv_parse_one, sw_match
|
2022-08-06 14:12:30 +00:00
|
|
|
from pySim.utils import sanitize_pin_adm, tabulate_str_list, boxed_heading_str, Hexstr
|
2021-09-22 14:53:22 +00:00
|
|
|
from pySim.card_handler import CardHandler, CardHandlerAuto
|
2021-01-08 22:29:35 +00:00
|
|
|
|
2022-07-16 09:53:21 +00:00
|
|
|
from pySim.filesystem import RuntimeState, CardDF, CardADF, CardModel
|
2021-11-08 15:12:03 +00:00
|
|
|
from pySim.profile import CardProfile
|
2021-01-08 22:29:35 +00:00
|
|
|
from pySim.ts_102_221 import CardProfileUICC
|
2021-10-19 19:44:24 +00:00
|
|
|
from pySim.ts_102_222 import Ts102222Commands
|
2021-04-02 18:27:05 +00:00
|
|
|
from pySim.ts_31_102 import CardApplicationUSIM
|
|
|
|
from pySim.ts_31_103 import CardApplicationISIM
|
2021-10-20 16:40:54 +00:00
|
|
|
from pySim.ara_m import CardApplicationARAM
|
2022-02-21 16:19:28 +00:00
|
|
|
from pySim.global_platform import CardApplicationISD
|
2021-10-20 08:14:18 +00:00
|
|
|
from pySim.gsm_r import DF_EIRENE
|
2022-08-06 14:12:30 +00:00
|
|
|
from pySim.cat import ProactiveCommand
|
2021-01-08 22:29:35 +00:00
|
|
|
|
2021-10-14 15:48:25 +00:00
|
|
|
# we need to import this module so that the SysmocomSJA2 sub-class of
|
|
|
|
# CardModel is created, which will add the ATR-based matching and
|
|
|
|
# calling of SysmocomSJA2.add_files. See CardModel.apply_matching_models
|
2021-10-14 13:53:39 +00:00
|
|
|
import pySim.sysmocom_sja2
|
|
|
|
|
2021-04-03 07:00:16 +00:00
|
|
|
from pySim.card_key_provider import CardKeyProviderCsv, card_key_provider_register, card_key_provider_get_field
|
2021-03-17 11:37:39 +00:00
|
|
|
|
2022-02-10 17:05:45 +00:00
|
|
|
|
2021-09-16 11:10:19 +00:00
|
|
|
def init_card(sl):
|
2022-02-10 17:05:45 +00:00
|
|
|
"""
|
|
|
|
Detect card in reader and setup card profile and runtime state. This
|
|
|
|
function must be called at least once on startup. The card and runtime
|
|
|
|
state object (rs) is required for all pySim-shell commands.
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Wait up to three seconds for a card in reader and try to detect
|
|
|
|
# the card type.
|
|
|
|
print("Waiting for card...")
|
|
|
|
try:
|
|
|
|
sl.wait_for_card(3)
|
|
|
|
except NoCardError:
|
|
|
|
print("No card detected!")
|
|
|
|
return None, None
|
|
|
|
except:
|
|
|
|
print("Card not readable!")
|
|
|
|
return None, None
|
|
|
|
|
2022-06-14 14:16:42 +00:00
|
|
|
generic_card = False
|
2022-02-10 17:05:45 +00:00
|
|
|
card = card_detect("auto", scc)
|
|
|
|
if card is None:
|
|
|
|
print("Warning: Could not detect card type - assuming a generic card type...")
|
|
|
|
card = SimCard(scc)
|
2022-06-14 14:16:42 +00:00
|
|
|
generic_card = True
|
2022-02-10 17:05:45 +00:00
|
|
|
|
|
|
|
profile = CardProfile.pick(scc)
|
|
|
|
if profile is None:
|
|
|
|
print("Unsupported card type!")
|
2022-06-01 15:58:38 +00:00
|
|
|
return None, card
|
2022-02-10 17:05:45 +00:00
|
|
|
|
2022-06-14 14:16:42 +00:00
|
|
|
# ETSI TS 102 221, Table 9.3 specifies a default for the PIN key
|
|
|
|
# references, however card manufactures may still decide to pick an
|
|
|
|
# arbitrary key reference. In case we run on a generic card class that is
|
|
|
|
# detected as an UICC, we will pick the key reference that is officially
|
|
|
|
# specified.
|
|
|
|
if generic_card and isinstance(profile, CardProfileUICC):
|
|
|
|
card._adm_chv_num = 0x0A
|
|
|
|
|
2022-02-10 17:05:45 +00:00
|
|
|
print("Info: Card is of type: %s" % str(profile))
|
|
|
|
|
|
|
|
# FIXME: This shouln't be here, the profile should add the applications,
|
|
|
|
# however, we cannot simply put his into ts_102_221.py since we would
|
|
|
|
# have to e.g. import CardApplicationUSIM from ts_31_102.py, which already
|
|
|
|
# imports from ts_102_221.py. This means we will end up with a circular
|
|
|
|
# import, which needs to be resolved first.
|
|
|
|
if isinstance(profile, CardProfileUICC):
|
|
|
|
profile.add_application(CardApplicationUSIM())
|
|
|
|
profile.add_application(CardApplicationISIM())
|
|
|
|
profile.add_application(CardApplicationARAM())
|
2022-02-21 16:19:28 +00:00
|
|
|
profile.add_application(CardApplicationISD())
|
2022-02-10 17:05:45 +00:00
|
|
|
|
|
|
|
# Create runtime state with card profile
|
|
|
|
rs = RuntimeState(card, profile)
|
|
|
|
|
|
|
|
# FIXME: This is an GSM-R related file, it needs to be added throughout,
|
|
|
|
# the profile. At the moment we add it for all cards, this won't hurt,
|
|
|
|
# but regular SIM and UICC will not have it and fail to select it.
|
|
|
|
rs.mf.add_file(DF_EIRENE())
|
|
|
|
|
|
|
|
CardModel.apply_matching_models(scc, rs)
|
|
|
|
|
|
|
|
# inform the transport that we can do context-specific SW interpretation
|
|
|
|
sl.set_sw_interpreter(rs)
|
|
|
|
|
|
|
|
return rs, card
|
|
|
|
|
2021-03-17 11:37:39 +00:00
|
|
|
|
2021-01-08 22:29:35 +00:00
|
|
|
class PysimApp(cmd2.Cmd):
|
2022-02-10 17:05:45 +00:00
|
|
|
CUSTOM_CATEGORY = 'pySim Commands'
|
|
|
|
|
|
|
|
def __init__(self, card, rs, sl, ch, script=None):
|
|
|
|
super().__init__(persistent_history_file='~/.pysim_shell_history', allow_cli_args=False,
|
|
|
|
use_ipython=True, auto_load_commands=False, startup_script=script)
|
|
|
|
self.intro = style('Welcome to pySim-shell!', fg=fg.red)
|
|
|
|
self.default_category = 'pySim-shell built-in commands'
|
|
|
|
self.card = None
|
|
|
|
self.rs = None
|
2022-07-17 12:23:17 +00:00
|
|
|
self.lchan = None
|
|
|
|
self.py_locals = {'card': self.card, 'rs': self.rs, 'lchan': self.lchan}
|
2022-02-10 17:05:45 +00:00
|
|
|
self.sl = sl
|
|
|
|
self.ch = ch
|
|
|
|
|
|
|
|
self.numeric_path = False
|
|
|
|
self.add_settable(cmd2.Settable('numeric_path', bool, 'Print File IDs instead of names',
|
|
|
|
onchange_cb=self._onchange_numeric_path))
|
|
|
|
self.conserve_write = True
|
|
|
|
self.add_settable(cmd2.Settable('conserve_write', bool, 'Read and compare before write',
|
|
|
|
onchange_cb=self._onchange_conserve_write))
|
|
|
|
self.json_pretty_print = True
|
|
|
|
self.add_settable(cmd2.Settable('json_pretty_print',
|
|
|
|
bool, 'Pretty-Print JSON output'))
|
|
|
|
self.apdu_trace = False
|
|
|
|
self.add_settable(cmd2.Settable('apdu_trace', bool, 'Trace and display APDUs exchanged with card',
|
|
|
|
onchange_cb=self._onchange_apdu_trace))
|
|
|
|
|
|
|
|
self.equip(card, rs)
|
|
|
|
|
|
|
|
def equip(self, card, rs):
|
|
|
|
"""
|
|
|
|
Equip pySim-shell with the supplied card and runtime state, add (or remove) all required settables and
|
|
|
|
and commands to enable card operations.
|
|
|
|
"""
|
|
|
|
|
|
|
|
rc = False
|
|
|
|
|
|
|
|
# Unequip everything from pySim-shell that would not work in unequipped state
|
|
|
|
if self.rs:
|
2022-07-17 12:23:17 +00:00
|
|
|
lchan = self.rs.lchan[0]
|
|
|
|
lchan.unregister_cmds(self)
|
2022-02-10 17:05:45 +00:00
|
|
|
for cmds in [Iso7816Commands, PySimCommands]:
|
|
|
|
cmd_set = self.find_commandsets(cmds)
|
|
|
|
if cmd_set:
|
|
|
|
self.unregister_command_set(cmd_set[0])
|
|
|
|
|
|
|
|
self.card = card
|
|
|
|
self.rs = rs
|
|
|
|
|
|
|
|
# When a card object and a runtime state is present, (re)equip pySim-shell with everything that is
|
|
|
|
# needed to operate on cards.
|
|
|
|
if self.card and self.rs:
|
2022-07-17 12:23:17 +00:00
|
|
|
self.lchan = self.rs.lchan[0]
|
2022-02-10 17:05:45 +00:00
|
|
|
self._onchange_conserve_write(
|
|
|
|
'conserve_write', False, self.conserve_write)
|
|
|
|
self._onchange_apdu_trace('apdu_trace', False, self.apdu_trace)
|
|
|
|
self.register_command_set(Iso7816Commands())
|
2021-10-19 19:44:24 +00:00
|
|
|
self.register_command_set(Ts102222Commands())
|
2022-02-10 17:05:45 +00:00
|
|
|
self.register_command_set(PySimCommands())
|
|
|
|
self.iccid, sw = self.card.read_iccid()
|
2022-07-17 12:23:17 +00:00
|
|
|
self.lchan.select('MF', self)
|
2022-02-10 17:05:45 +00:00
|
|
|
rc = True
|
|
|
|
else:
|
|
|
|
self.poutput("pySim-shell not equipped!")
|
|
|
|
|
|
|
|
self.update_prompt()
|
|
|
|
return rc
|
|
|
|
|
|
|
|
def poutput_json(self, data, force_no_pretty=False):
|
|
|
|
"""like cmd2.poutput() but for a JSON serializable dict."""
|
|
|
|
if force_no_pretty or self.json_pretty_print == False:
|
|
|
|
output = json.dumps(data, cls=JsonEncoder)
|
|
|
|
else:
|
|
|
|
output = json.dumps(data, cls=JsonEncoder, indent=4)
|
|
|
|
self.poutput(output)
|
|
|
|
|
|
|
|
def _onchange_numeric_path(self, param_name, old, new):
|
|
|
|
self.update_prompt()
|
|
|
|
|
|
|
|
def _onchange_conserve_write(self, param_name, old, new):
|
|
|
|
if self.rs:
|
|
|
|
self.rs.conserve_write = new
|
|
|
|
|
|
|
|
def _onchange_apdu_trace(self, param_name, old, new):
|
|
|
|
if self.card:
|
|
|
|
if new == True:
|
|
|
|
self.card._scc._tp.apdu_tracer = self.Cmd2ApduTracer(self)
|
|
|
|
else:
|
|
|
|
self.card._scc._tp.apdu_tracer = None
|
|
|
|
|
|
|
|
class Cmd2ApduTracer(ApduTracer):
|
|
|
|
def __init__(self, cmd2_app):
|
|
|
|
self.cmd2 = app
|
|
|
|
|
|
|
|
def trace_response(self, cmd, sw, resp):
|
|
|
|
self.cmd2.poutput("-> %s %s" % (cmd[:10], cmd[10:]))
|
|
|
|
self.cmd2.poutput("<- %s: %s" % (sw, resp))
|
|
|
|
|
|
|
|
def update_prompt(self):
|
2022-07-17 12:23:17 +00:00
|
|
|
if self.lchan:
|
2022-07-19 21:48:45 +00:00
|
|
|
path_str = self.lchan.selected_file.fully_qualified_path_str(not self.numeric_path)
|
|
|
|
self.prompt = 'pySIM-shell (%s)> ' % (path_str)
|
2022-02-10 17:05:45 +00:00
|
|
|
else:
|
2022-06-01 15:58:38 +00:00
|
|
|
if self.card:
|
|
|
|
self.prompt = 'pySIM-shell (no card profile)> '
|
|
|
|
else:
|
|
|
|
self.prompt = 'pySIM-shell (no card)> '
|
2022-02-10 17:05:45 +00:00
|
|
|
|
|
|
|
@cmd2.with_category(CUSTOM_CATEGORY)
|
|
|
|
def do_intro(self, _):
|
|
|
|
"""Display the intro banner"""
|
|
|
|
self.poutput(self.intro)
|
|
|
|
|
|
|
|
def do_eof(self, _: argparse.Namespace) -> bool:
|
|
|
|
self.poutput("")
|
|
|
|
return self.do_quit('')
|
|
|
|
|
|
|
|
@cmd2.with_category(CUSTOM_CATEGORY)
|
|
|
|
def do_equip(self, opts):
|
|
|
|
"""Equip pySim-shell with card"""
|
|
|
|
rs, card = init_card(sl)
|
|
|
|
self.equip(card, rs)
|
|
|
|
|
2022-06-01 15:58:38 +00:00
|
|
|
apdu_cmd_parser = argparse.ArgumentParser()
|
|
|
|
apdu_cmd_parser.add_argument('APDU', type=str, help='APDU as hex string')
|
2022-06-01 16:05:34 +00:00
|
|
|
apdu_cmd_parser.add_argument('--expect-sw', help='expect a specified status word', type=str, default=None)
|
2022-06-01 15:58:38 +00:00
|
|
|
|
|
|
|
@cmd2.with_argparser(apdu_cmd_parser)
|
|
|
|
def do_apdu(self, opts):
|
|
|
|
"""Send a raw APDU to the card, and print SW + Response.
|
|
|
|
DANGEROUS: pySim-shell will not know any card state changes, and
|
|
|
|
not continue to work as expected if you e.g. select a different
|
|
|
|
file."""
|
|
|
|
data, sw = self.card._scc._tp.send_apdu(opts.APDU)
|
|
|
|
if data:
|
|
|
|
self.poutput("SW: %s, RESP: %s" % (sw, data))
|
|
|
|
else:
|
|
|
|
self.poutput("SW: %s" % sw)
|
2022-06-01 16:05:34 +00:00
|
|
|
if opts.expect_sw:
|
|
|
|
if not sw_match(sw, opts.expect_sw):
|
|
|
|
raise SwMatchError(sw, opts.expect_sw)
|
2022-06-01 15:58:38 +00:00
|
|
|
|
2022-02-10 17:05:45 +00:00
|
|
|
class InterceptStderr(list):
|
|
|
|
def __init__(self):
|
|
|
|
self._stderr_backup = sys.stderr
|
|
|
|
|
|
|
|
def __enter__(self):
|
|
|
|
self._stringio_stderr = StringIO()
|
|
|
|
sys.stderr = self._stringio_stderr
|
|
|
|
return self
|
|
|
|
|
|
|
|
def __exit__(self, *args):
|
|
|
|
self.stderr = self._stringio_stderr.getvalue().strip()
|
|
|
|
del self._stringio_stderr
|
|
|
|
sys.stderr = self._stderr_backup
|
|
|
|
|
|
|
|
def _show_failure_sign(self):
|
|
|
|
self.poutput(style(" +-------------+", fg=fg.bright_red))
|
|
|
|
self.poutput(style(" + ## ## +", fg=fg.bright_red))
|
|
|
|
self.poutput(style(" + ## ## +", fg=fg.bright_red))
|
|
|
|
self.poutput(style(" + ### +", fg=fg.bright_red))
|
|
|
|
self.poutput(style(" + ## ## +", fg=fg.bright_red))
|
|
|
|
self.poutput(style(" + ## ## +", fg=fg.bright_red))
|
|
|
|
self.poutput(style(" +-------------+", fg=fg.bright_red))
|
|
|
|
self.poutput("")
|
|
|
|
|
|
|
|
def _show_success_sign(self):
|
|
|
|
self.poutput(style(" +-------------+", fg=fg.bright_green))
|
|
|
|
self.poutput(style(" + ## +", fg=fg.bright_green))
|
|
|
|
self.poutput(style(" + ## +", fg=fg.bright_green))
|
|
|
|
self.poutput(style(" + # ## +", fg=fg.bright_green))
|
|
|
|
self.poutput(style(" + ## # +", fg=fg.bright_green))
|
|
|
|
self.poutput(style(" + ## +", fg=fg.bright_green))
|
|
|
|
self.poutput(style(" +-------------+", fg=fg.bright_green))
|
|
|
|
self.poutput("")
|
|
|
|
|
|
|
|
def _process_card(self, first, script_path):
|
|
|
|
|
|
|
|
# Early phase of card initialzation (this part may fail with an exception)
|
|
|
|
try:
|
|
|
|
rs, card = init_card(self.sl)
|
|
|
|
rc = self.equip(card, rs)
|
|
|
|
except:
|
|
|
|
self.poutput("")
|
|
|
|
self.poutput("Card initialization failed with an exception:")
|
|
|
|
self.poutput("---------------------8<---------------------")
|
|
|
|
traceback.print_exc()
|
|
|
|
self.poutput("---------------------8<---------------------")
|
|
|
|
self.poutput("")
|
|
|
|
return -1
|
|
|
|
|
|
|
|
# Actual card processing step. This part should never fail with an exception since the cmd2
|
|
|
|
# do_run_script method will catch any exception that might occur during script execution.
|
|
|
|
if rc:
|
|
|
|
self.poutput("")
|
|
|
|
self.poutput("Transcript stdout:")
|
|
|
|
self.poutput("---------------------8<---------------------")
|
|
|
|
with self.InterceptStderr() as logged:
|
|
|
|
self.do_run_script(script_path)
|
|
|
|
self.poutput("---------------------8<---------------------")
|
|
|
|
|
|
|
|
self.poutput("")
|
|
|
|
self.poutput("Transcript stderr:")
|
|
|
|
if logged.stderr:
|
|
|
|
self.poutput("---------------------8<---------------------")
|
|
|
|
self.poutput(logged.stderr)
|
|
|
|
self.poutput("---------------------8<---------------------")
|
|
|
|
else:
|
|
|
|
self.poutput("(none)")
|
|
|
|
|
|
|
|
# Check for exceptions
|
|
|
|
self.poutput("")
|
|
|
|
if "EXCEPTION of type" not in logged.stderr:
|
|
|
|
return 0
|
|
|
|
|
|
|
|
return -1
|
|
|
|
|
|
|
|
bulk_script_parser = argparse.ArgumentParser()
|
|
|
|
bulk_script_parser.add_argument(
|
|
|
|
'script_path', help="path to the script file")
|
|
|
|
bulk_script_parser.add_argument('--halt_on_error', help='stop card handling if an exeption occurs',
|
|
|
|
action='store_true')
|
|
|
|
bulk_script_parser.add_argument('--tries', type=int, default=2,
|
|
|
|
help='how many tries before trying the next card')
|
|
|
|
bulk_script_parser.add_argument('--on_stop_action', type=str, default=None,
|
|
|
|
help='commandline to execute when card handling has stopped')
|
|
|
|
bulk_script_parser.add_argument('--pre_card_action', type=str, default=None,
|
|
|
|
help='commandline to execute before actually talking to the card')
|
|
|
|
|
|
|
|
@cmd2.with_argparser(bulk_script_parser)
|
|
|
|
@cmd2.with_category(CUSTOM_CATEGORY)
|
|
|
|
def do_bulk_script(self, opts):
|
|
|
|
"""Run script on multiple cards (bulk provisioning)"""
|
|
|
|
|
|
|
|
# Make sure that the script file exists and that it is readable.
|
|
|
|
if not os.access(opts.script_path, os.R_OK):
|
|
|
|
self.poutput("Invalid script file!")
|
|
|
|
return
|
|
|
|
|
|
|
|
success_count = 0
|
|
|
|
fail_count = 0
|
|
|
|
|
|
|
|
first = True
|
|
|
|
while 1:
|
|
|
|
# TODO: Count consecutive failures, if more than N consecutive failures occur, then stop.
|
|
|
|
# The ratinale is: There may be a problem with the device, we do want to prevent that
|
|
|
|
# all remaining cards are fired to the error bin. This is only relevant for situations
|
|
|
|
# with large stacks, probably we do not need this feature right now.
|
|
|
|
|
|
|
|
try:
|
|
|
|
# In case of failure, try multiple times.
|
|
|
|
for i in range(opts.tries):
|
|
|
|
# fetch card into reader bay
|
|
|
|
ch.get(first)
|
|
|
|
|
|
|
|
# if necessary execute an action before we start processing the card
|
|
|
|
if(opts.pre_card_action):
|
|
|
|
os.system(opts.pre_card_action)
|
|
|
|
|
|
|
|
# process the card
|
|
|
|
rc = self._process_card(first, opts.script_path)
|
|
|
|
if rc == 0:
|
|
|
|
success_count = success_count + 1
|
|
|
|
self._show_success_sign()
|
|
|
|
self.poutput("Statistics: success :%i, failure: %i" % (
|
|
|
|
success_count, fail_count))
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
fail_count = fail_count + 1
|
|
|
|
self._show_failure_sign()
|
|
|
|
self.poutput("Statistics: success :%i, failure: %i" % (
|
|
|
|
success_count, fail_count))
|
|
|
|
|
|
|
|
# Depending on success or failure, the card goes either in the "error" bin or in the
|
|
|
|
# "done" bin.
|
|
|
|
if rc < 0:
|
|
|
|
ch.error()
|
|
|
|
else:
|
|
|
|
ch.done()
|
|
|
|
|
|
|
|
# In most cases it is possible to proceed with the next card, but the
|
|
|
|
# user may decide to halt immediately when an error occurs
|
|
|
|
if opts.halt_on_error and rc < 0:
|
|
|
|
return
|
|
|
|
|
|
|
|
except (KeyboardInterrupt):
|
|
|
|
self.poutput("")
|
|
|
|
self.poutput("Terminated by user!")
|
|
|
|
return
|
|
|
|
except (SystemExit):
|
|
|
|
# When all cards are processed the card handler device will throw a SystemExit
|
|
|
|
# exception. Also Errors that are not recoverable (cards stuck etc.) will end up here.
|
|
|
|
# The user has the option to execute some action to make aware that the card handler
|
|
|
|
# needs service.
|
|
|
|
if(opts.on_stop_action):
|
|
|
|
os.system(opts.on_stop_action)
|
|
|
|
return
|
|
|
|
except:
|
|
|
|
self.poutput("")
|
|
|
|
self.poutput("Card handling failed with an exception:")
|
|
|
|
self.poutput("---------------------8<---------------------")
|
|
|
|
traceback.print_exc()
|
|
|
|
self.poutput("---------------------8<---------------------")
|
|
|
|
self.poutput("")
|
|
|
|
fail_count = fail_count + 1
|
|
|
|
self._show_failure_sign()
|
|
|
|
self.poutput("Statistics: success :%i, failure: %i" %
|
|
|
|
(success_count, fail_count))
|
|
|
|
|
|
|
|
first = False
|
|
|
|
|
|
|
|
echo_parser = argparse.ArgumentParser()
|
|
|
|
echo_parser.add_argument('string', help="string to echo on the shell")
|
|
|
|
|
|
|
|
@cmd2.with_argparser(echo_parser)
|
|
|
|
@cmd2.with_category(CUSTOM_CATEGORY)
|
|
|
|
def do_echo(self, opts):
|
|
|
|
"""Echo (print) a string on the console"""
|
|
|
|
self.poutput(opts.string)
|
|
|
|
|
2022-07-23 10:49:14 +00:00
|
|
|
@cmd2.with_category(CUSTOM_CATEGORY)
|
|
|
|
def do_version(self, opts):
|
|
|
|
"""Print the pySim software version."""
|
|
|
|
import pkg_resources
|
|
|
|
self.poutput(pkg_resources.get_distribution('pySim'))
|
2021-01-08 22:29:35 +00:00
|
|
|
|
2021-04-03 08:47:29 +00:00
|
|
|
@with_default_category('pySim Commands')
|
|
|
|
class PySimCommands(CommandSet):
|
2022-02-10 17:05:45 +00:00
|
|
|
def __init__(self):
|
|
|
|
super().__init__()
|
|
|
|
|
|
|
|
dir_parser = argparse.ArgumentParser()
|
|
|
|
dir_parser.add_argument(
|
|
|
|
'--fids', help='Show file identifiers', action='store_true')
|
|
|
|
dir_parser.add_argument(
|
|
|
|
'--names', help='Show file names', action='store_true')
|
|
|
|
dir_parser.add_argument(
|
|
|
|
'--apps', help='Show applications', action='store_true')
|
|
|
|
dir_parser.add_argument(
|
|
|
|
'--all', help='Show all selectable identifiers and names', action='store_true')
|
|
|
|
|
|
|
|
@cmd2.with_argparser(dir_parser)
|
|
|
|
def do_dir(self, opts):
|
|
|
|
"""Show a listing of files available in currently selected DF or MF"""
|
|
|
|
if opts.all:
|
|
|
|
flags = []
|
|
|
|
elif opts.fids or opts.names or opts.apps:
|
|
|
|
flags = ['PARENT', 'SELF']
|
|
|
|
if opts.fids:
|
|
|
|
flags += ['FIDS', 'AIDS']
|
|
|
|
if opts.names:
|
|
|
|
flags += ['FNAMES', 'ANAMES']
|
|
|
|
if opts.apps:
|
|
|
|
flags += ['ANAMES', 'AIDS']
|
|
|
|
else:
|
|
|
|
flags = ['PARENT', 'SELF', 'FNAMES', 'ANAMES']
|
|
|
|
selectables = list(
|
2022-07-17 12:23:17 +00:00
|
|
|
self._cmd.lchan.selected_file.get_selectable_names(flags=flags))
|
2022-02-10 17:05:45 +00:00
|
|
|
directory_str = tabulate_str_list(
|
|
|
|
selectables, width=79, hspace=2, lspace=1, align_left=True)
|
2022-07-19 21:48:45 +00:00
|
|
|
path = self._cmd.lchan.selected_file.fully_qualified_path_str(True)
|
|
|
|
self._cmd.poutput(path)
|
|
|
|
path = self._cmd.lchan.selected_file.fully_qualified_path_str(False)
|
|
|
|
self._cmd.poutput(path)
|
2022-02-10 17:05:45 +00:00
|
|
|
self._cmd.poutput(directory_str)
|
|
|
|
self._cmd.poutput("%d files" % len(selectables))
|
|
|
|
|
2022-05-31 11:42:56 +00:00
|
|
|
def walk(self, indent=0, action_ef=None, action_df=None, context=None, **kwargs):
|
2022-02-10 17:05:45 +00:00
|
|
|
"""Recursively walk through the file system, starting at the currently selected DF"""
|
2022-05-31 11:42:56 +00:00
|
|
|
|
2022-07-17 12:23:17 +00:00
|
|
|
if isinstance(self._cmd.lchan.selected_file, CardDF):
|
2022-05-31 11:42:56 +00:00
|
|
|
if action_df:
|
|
|
|
action_df(context, opts)
|
|
|
|
|
2022-07-17 12:23:17 +00:00
|
|
|
files = self._cmd.lchan.selected_file.get_selectables(
|
2022-02-10 17:05:45 +00:00
|
|
|
flags=['FNAMES', 'ANAMES'])
|
|
|
|
for f in files:
|
2022-05-31 11:42:56 +00:00
|
|
|
# special case: When no action is performed, just output a directory
|
|
|
|
if not action_ef and not action_df:
|
2022-02-10 17:05:45 +00:00
|
|
|
output_str = " " * indent + str(f) + (" " * 250)
|
|
|
|
output_str = output_str[0:25]
|
|
|
|
if isinstance(files[f], CardADF):
|
|
|
|
output_str += " " + str(files[f].aid)
|
|
|
|
else:
|
|
|
|
output_str += " " + str(files[f].fid)
|
|
|
|
output_str += " " + str(files[f].desc)
|
|
|
|
self._cmd.poutput(output_str)
|
|
|
|
|
|
|
|
if isinstance(files[f], CardDF):
|
|
|
|
skip_df = False
|
|
|
|
try:
|
2022-07-17 12:23:17 +00:00
|
|
|
fcp_dec = self._cmd.lchan.select(f, self._cmd)
|
2022-02-10 17:05:45 +00:00
|
|
|
except Exception as e:
|
|
|
|
skip_df = True
|
2022-07-17 12:23:17 +00:00
|
|
|
df = self._cmd.lchan.selected_file
|
2022-07-19 21:48:45 +00:00
|
|
|
df_path = df.fully_qualified_path_str(True)
|
|
|
|
df_skip_reason_str = df_path + \
|
2022-02-10 17:05:45 +00:00
|
|
|
"/" + str(f) + ", " + str(e)
|
|
|
|
if context:
|
|
|
|
context['DF_SKIP'] += 1
|
|
|
|
context['DF_SKIP_REASON'].append(df_skip_reason_str)
|
|
|
|
|
|
|
|
# If the DF was skipped, we never have entered the directory
|
|
|
|
# below, so we must not move up.
|
|
|
|
if skip_df == False:
|
2022-05-31 11:42:56 +00:00
|
|
|
self.walk(indent + 1, action_ef, action_df, context, **kwargs)
|
2022-07-17 12:23:17 +00:00
|
|
|
fcp_dec = self._cmd.lchan.select("..", self._cmd)
|
2022-02-10 17:05:45 +00:00
|
|
|
|
2022-05-31 11:42:56 +00:00
|
|
|
elif action_ef:
|
2022-07-17 12:23:17 +00:00
|
|
|
df_before_action = self._cmd.lchan.selected_file
|
2022-05-31 11:42:56 +00:00
|
|
|
action_ef(f, context, **kwargs)
|
2022-02-10 17:05:45 +00:00
|
|
|
# When walking through the file system tree the action must not
|
|
|
|
# always restore the currently selected file to the file that
|
|
|
|
# was selected before executing the action() callback.
|
2022-07-17 12:23:17 +00:00
|
|
|
if df_before_action != self._cmd.lchan.selected_file:
|
2022-02-10 17:05:45 +00:00
|
|
|
raise RuntimeError("inconsistent walk, %s is currently selected but expecting %s to be selected"
|
2022-07-17 12:23:17 +00:00
|
|
|
% (str(self._cmd.lchan.selected_file), str(df_before_action)))
|
2022-02-10 17:05:45 +00:00
|
|
|
|
|
|
|
def do_tree(self, opts):
|
|
|
|
"""Display a filesystem-tree with all selectable files"""
|
|
|
|
self.walk()
|
|
|
|
|
2022-05-31 11:42:56 +00:00
|
|
|
def export_ef(self, filename, context, as_json):
|
|
|
|
""" Select and export a single elementary file (EF) """
|
2022-02-10 17:05:45 +00:00
|
|
|
context['COUNT'] += 1
|
2022-07-17 12:23:17 +00:00
|
|
|
df = self._cmd.lchan.selected_file
|
2022-02-10 17:05:45 +00:00
|
|
|
|
2022-05-19 08:13:30 +00:00
|
|
|
# The currently selected file (not the file we are going to export)
|
|
|
|
# must always be an ADF or DF. From this starting point we select
|
|
|
|
# the EF we want to export. To maintain consistency we will then
|
|
|
|
# select the current DF again (see comment below).
|
2022-02-10 17:05:45 +00:00
|
|
|
if not isinstance(df, CardDF):
|
|
|
|
raise RuntimeError(
|
|
|
|
"currently selected file %s is not a DF or ADF" % str(df))
|
|
|
|
|
|
|
|
df_path_list = df.fully_qualified_path(True)
|
2022-07-19 21:48:45 +00:00
|
|
|
df_path = df.fully_qualified_path_str(True)
|
|
|
|
df_path_fid = df.fully_qualified_path_str(False)
|
2022-02-10 17:05:45 +00:00
|
|
|
|
2022-07-19 21:48:45 +00:00
|
|
|
file_str = df_path + "/" + str(filename)
|
2022-02-10 17:05:45 +00:00
|
|
|
self._cmd.poutput(boxed_heading_str(file_str))
|
|
|
|
|
2022-07-19 21:48:45 +00:00
|
|
|
self._cmd.poutput("# directory: %s (%s)" % (df_path, df_path_fid))
|
2022-02-10 17:05:45 +00:00
|
|
|
try:
|
2022-07-17 12:23:17 +00:00
|
|
|
fcp_dec = self._cmd.lchan.select(filename, self._cmd)
|
2022-02-10 17:05:45 +00:00
|
|
|
self._cmd.poutput("# file: %s (%s)" % (
|
2022-07-17 12:23:17 +00:00
|
|
|
self._cmd.lchan.selected_file.name, self._cmd.lchan.selected_file.fid))
|
2022-02-10 17:05:45 +00:00
|
|
|
|
2022-07-17 12:23:17 +00:00
|
|
|
structure = self._cmd.lchan.selected_file_structure()
|
2022-02-10 17:05:45 +00:00
|
|
|
self._cmd.poutput("# structure: %s" % str(structure))
|
2022-07-17 12:23:17 +00:00
|
|
|
self._cmd.poutput("# RAW FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp_hex))
|
|
|
|
self._cmd.poutput("# Decoded FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp))
|
2022-02-10 17:05:45 +00:00
|
|
|
|
|
|
|
for f in df_path_list:
|
|
|
|
self._cmd.poutput("select " + str(f))
|
2022-07-17 12:23:17 +00:00
|
|
|
self._cmd.poutput("select " + self._cmd.lchan.selected_file.name)
|
2022-02-10 17:05:45 +00:00
|
|
|
|
|
|
|
if structure == 'transparent':
|
2022-02-10 17:56:41 +00:00
|
|
|
if as_json:
|
2022-07-17 12:23:17 +00:00
|
|
|
result = self._cmd.lchan.read_binary_dec()
|
2022-02-10 17:56:41 +00:00
|
|
|
self._cmd.poutput("update_binary_decoded '%s'" % json.dumps(result[0], cls=JsonEncoder))
|
|
|
|
else:
|
2022-07-17 12:23:17 +00:00
|
|
|
result = self._cmd.lchan.read_binary()
|
2022-02-10 17:56:41 +00:00
|
|
|
self._cmd.poutput("update_binary " + str(result[0]))
|
2022-02-10 17:05:45 +00:00
|
|
|
elif structure == 'cyclic' or structure == 'linear_fixed':
|
|
|
|
# Use number of records specified in select response
|
2022-07-17 12:23:17 +00:00
|
|
|
num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
|
2022-02-13 16:52:28 +00:00
|
|
|
if num_of_rec:
|
2022-02-10 17:05:45 +00:00
|
|
|
for r in range(1, num_of_rec + 1):
|
2022-02-10 17:56:41 +00:00
|
|
|
if as_json:
|
2022-07-17 12:23:17 +00:00
|
|
|
result = self._cmd.lchan.read_record_dec(r)
|
2022-02-10 17:56:41 +00:00
|
|
|
self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
|
|
|
|
else:
|
2022-07-17 12:23:17 +00:00
|
|
|
result = self._cmd.lchan.read_record(r)
|
2022-02-10 17:56:41 +00:00
|
|
|
self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
|
|
|
|
|
2022-02-10 17:05:45 +00:00
|
|
|
# When the select response does not return the number of records, read until we hit the
|
|
|
|
# first record that cannot be read.
|
|
|
|
else:
|
|
|
|
r = 1
|
|
|
|
while True:
|
|
|
|
try:
|
2022-02-10 17:56:41 +00:00
|
|
|
if as_json:
|
2022-07-17 12:23:17 +00:00
|
|
|
result = self._cmd.lchan.read_record_dec(r)
|
2022-02-10 17:56:41 +00:00
|
|
|
self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
|
|
|
|
else:
|
2022-07-17 12:23:17 +00:00
|
|
|
result = self._cmd.lchan.read_record(r)
|
2022-02-10 17:56:41 +00:00
|
|
|
self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
|
2022-02-10 17:05:45 +00:00
|
|
|
except SwMatchError as e:
|
|
|
|
# We are past the last valid record - stop
|
|
|
|
if e.sw_actual == "9402":
|
|
|
|
break
|
|
|
|
# Some other problem occurred
|
|
|
|
else:
|
|
|
|
raise e
|
|
|
|
r = r + 1
|
|
|
|
elif structure == 'ber_tlv':
|
2022-07-17 12:23:17 +00:00
|
|
|
tags = self._cmd.lchan.retrieve_tags()
|
2022-02-10 17:05:45 +00:00
|
|
|
for t in tags:
|
2022-07-17 12:23:17 +00:00
|
|
|
result = self._cmd.lchan.retrieve_data(t)
|
2022-02-10 17:05:45 +00:00
|
|
|
(tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
|
|
|
|
self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val)))
|
|
|
|
else:
|
|
|
|
raise RuntimeError(
|
|
|
|
'Unsupported structure "%s" of file "%s"' % (structure, filename))
|
|
|
|
except Exception as e:
|
2022-07-19 21:48:45 +00:00
|
|
|
bad_file_str = df_path + "/" + str(filename) + ", " + str(e)
|
2022-02-10 17:05:45 +00:00
|
|
|
self._cmd.poutput("# bad file: %s" % bad_file_str)
|
|
|
|
context['ERR'] += 1
|
|
|
|
context['BAD'].append(bad_file_str)
|
|
|
|
|
|
|
|
# When reading the file is done, make sure the parent file is
|
|
|
|
# selected again. This will be the usual case, however we need
|
|
|
|
# to check before since we must not select the same DF twice
|
2022-07-17 12:23:17 +00:00
|
|
|
if df != self._cmd.lchan.selected_file:
|
|
|
|
self._cmd.lchan.select(df.fid or df.aid, self._cmd)
|
2022-02-10 17:05:45 +00:00
|
|
|
|
|
|
|
self._cmd.poutput("#")
|
|
|
|
|
|
|
|
export_parser = argparse.ArgumentParser()
|
|
|
|
export_parser.add_argument(
|
|
|
|
'--filename', type=str, default=None, help='only export specific file')
|
2022-02-10 17:56:41 +00:00
|
|
|
export_parser.add_argument(
|
|
|
|
'--json', action='store_true', help='export as JSON (less reliable)')
|
2022-02-10 17:05:45 +00:00
|
|
|
|
|
|
|
@cmd2.with_argparser(export_parser)
|
|
|
|
def do_export(self, opts):
|
|
|
|
"""Export files to script that can be imported back later"""
|
|
|
|
context = {'ERR': 0, 'COUNT': 0, 'BAD': [],
|
|
|
|
'DF_SKIP': 0, 'DF_SKIP_REASON': []}
|
2022-05-19 08:20:30 +00:00
|
|
|
kwargs_export = {'as_json': opts.json}
|
2022-05-31 12:08:47 +00:00
|
|
|
exception_str_add = ""
|
|
|
|
|
2022-02-10 17:05:45 +00:00
|
|
|
if opts.filename:
|
2022-05-31 11:42:56 +00:00
|
|
|
self.export_ef(opts.filename, context, **kwargs_export)
|
2022-02-10 17:05:45 +00:00
|
|
|
else:
|
2022-05-31 12:08:47 +00:00
|
|
|
try:
|
|
|
|
self.walk(0, self.export_ef, None, context, **kwargs_export)
|
|
|
|
except Exception as e:
|
|
|
|
print("# Stopping early here due to exception: " + str(e))
|
|
|
|
print("#")
|
|
|
|
exception_str_add = ", also had to stop early due to exception:" + str(e)
|
2022-02-10 17:05:45 +00:00
|
|
|
|
|
|
|
self._cmd.poutput(boxed_heading_str("Export summary"))
|
|
|
|
|
|
|
|
self._cmd.poutput("# total files visited: %u" % context['COUNT'])
|
|
|
|
self._cmd.poutput("# bad files: %u" % context['ERR'])
|
|
|
|
for b in context['BAD']:
|
|
|
|
self._cmd.poutput("# " + b)
|
|
|
|
|
|
|
|
self._cmd.poutput("# skipped dedicated files(s): %u" %
|
|
|
|
context['DF_SKIP'])
|
|
|
|
for b in context['DF_SKIP_REASON']:
|
|
|
|
self._cmd.poutput("# " + b)
|
|
|
|
|
|
|
|
if context['ERR'] and context['DF_SKIP']:
|
2022-05-31 12:08:47 +00:00
|
|
|
raise RuntimeError("unable to export %i elementary file(s) and %i dedicated file(s)%s" % (
|
|
|
|
context['ERR'], context['DF_SKIP'], exception_str_add))
|
2022-02-10 17:05:45 +00:00
|
|
|
elif context['ERR']:
|
|
|
|
raise RuntimeError(
|
2022-05-31 12:08:47 +00:00
|
|
|
"unable to export %i elementary file(s)%s" % (context['ERR'], exception_str_add))
|
2022-02-10 17:05:45 +00:00
|
|
|
elif context['DF_SKIP']:
|
|
|
|
raise RuntimeError(
|
2022-05-31 12:08:47 +00:00
|
|
|
"unable to export %i dedicated files(s)%s" % (context['ERR'], exception_str_add))
|
2022-02-10 17:05:45 +00:00
|
|
|
|
|
|
|
def do_reset(self, opts):
|
|
|
|
"""Reset the Card."""
|
2022-07-17 12:23:17 +00:00
|
|
|
atr = self._cmd.lchan.reset(self._cmd)
|
2022-02-10 17:05:45 +00:00
|
|
|
self._cmd.poutput('Card ATR: %s' % atr)
|
|
|
|
self._cmd.update_prompt()
|
|
|
|
|
|
|
|
def do_desc(self, opts):
|
|
|
|
"""Display human readable file description for the currently selected file"""
|
|