blob: db9c32753f078c504ba4588bf4a21be43b9e9524 [file] [log] [blame]
Daniel Willmannde07b952020-10-19 10:32:34 +02001#!/usr/bin/env python3
Sylvain Munaut76504e02010-12-07 00:24:32 +01002
3#
4# Utility to deal with sim cards and program the 'magic' ones easily
5#
6#
7# Part of the sim link code of inspired by pySimReader-Serial-src-v2
8#
9#
10# Copyright (C) 2009 Sylvain Munaut <tnt@246tNt.com>
11# Copyright (C) 2010 Harald Welte <laforge@gnumonks.org>
12#
13# This program is free software: you can redistribute it and/or modify
14# it under the terms of the GNU General Public License as published by
15# the Free Software Foundation, either version 2 of the License, or
16# (at your option) any later version.
17#
18# This program is distributed in the hope that it will be useful,
19# but WITHOUT ANY WARRANTY; without even the implied warranty of
20# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21# GNU General Public License for more details.
22#
23# You should have received a copy of the GNU General Public License
24# along with this program. If not, see <http://www.gnu.org/licenses/>.
25#
26
27import hashlib
Harald Welte3aa0b412024-03-14 16:22:23 +010028import argparse
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +010029import os
Sylvain Munaut76504e02010-12-07 00:24:32 +010030import random
31import re
32import sys
Philipp Maierc5b422e2019-08-30 11:41:02 +020033import traceback
Denis 'GNUtoo' Carikli79f5b602020-02-15 04:02:57 +070034import json
Philipp Maier2688ddf2022-12-16 13:36:42 +010035import csv
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +010036
Sylvain Munaut76504e02010-12-07 00:24:32 +010037from pySim.commands import SimCardCommands
Harald Welte3aa0b412024-03-14 16:22:23 +010038from pySim.transport import init_reader, argparse_add_reader_args
Harald Weltef8d2e2b2023-07-09 17:58:38 +020039from pySim.legacy.cards import _cards_classes, card_detect
Harald Welte6e0458d2021-04-03 11:52:37 +020040from pySim.utils import h2b, swap_nibbles, rpad, derive_milenage_opc, calculate_luhn, dec_iccid
Harald Welte57ad38e2023-07-09 22:14:09 +020041from pySim.ts_51_011 import EF_AD
42from pySim.legacy.ts_51_011 import EF
Philipp Maierc5b422e2019-08-30 11:41:02 +020043from pySim.card_handler import *
Philipp Maier7592eee2019-09-12 13:03:23 +020044from pySim.utils import *
Sylvain Munaut76504e02010-12-07 00:24:32 +010045
Harald Weltec91085e2022-02-10 18:05:45 +010046
Sylvain Munaut76504e02010-12-07 00:24:32 +010047def parse_options():
48
Harald Welte3aa0b412024-03-14 16:22:23 +010049 parser = argparse.ArgumentParser()
50 argparse_add_reader_args(parser)
Sylvain Munaut76504e02010-12-07 00:24:32 +010051
Harald Welte3aa0b412024-03-14 16:22:23 +010052 parser.add_argument("-t", "--type", dest="type",
53 help="Card type (user -t list to view) [default: %(default)s]",
Harald Weltec91085e2022-02-10 18:05:45 +010054 default="auto",
55 )
Harald Welte3aa0b412024-03-14 16:22:23 +010056 parser.add_argument("-T", "--probe", dest="probe",
Harald Weltec91085e2022-02-10 18:05:45 +010057 help="Determine card type",
58 default=False, action="store_true"
59 )
Harald Welte3aa0b412024-03-14 16:22:23 +010060 parser.add_argument("-a", "--pin-adm", dest="pin_adm",
Harald Weltec91085e2022-02-10 18:05:45 +010061 help="ADM PIN used for provisioning (overwrites default)",
62 )
Harald Welte3aa0b412024-03-14 16:22:23 +010063 parser.add_argument("-A", "--pin-adm-hex", dest="pin_adm_hex",
Harald Weltec91085e2022-02-10 18:05:45 +010064 help="ADM PIN used for provisioning, as hex string (16 characters long",
65 )
Harald Welte3aa0b412024-03-14 16:22:23 +010066 parser.add_argument("-e", "--erase", dest="erase", action='store_true',
67 help="Erase beforehand [default: %(default)s]",
Harald Weltec91085e2022-02-10 18:05:45 +010068 default=False,
69 )
Sylvain Munaut76504e02010-12-07 00:24:32 +010070
Harald Welte3aa0b412024-03-14 16:22:23 +010071 parser.add_argument("-S", "--source", dest="source",
72 help="Data Source[default: %(default)s]",
Harald Weltec91085e2022-02-10 18:05:45 +010073 default="cmdline",
74 )
Harald Welte7f62cec2012-08-13 20:07:41 +020075
Harald Weltec91085e2022-02-10 18:05:45 +010076 # if mode is "cmdline"
Harald Welte3aa0b412024-03-14 16:22:23 +010077 parser.add_argument("-n", "--name", dest="name",
78 help="Operator name [default: %(default)s]",
Harald Weltec91085e2022-02-10 18:05:45 +010079 default="Magic",
80 )
Harald Welte3aa0b412024-03-14 16:22:23 +010081 parser.add_argument("-c", "--country", dest="country", type=int, metavar="CC",
82 help="Country code [default: %(default)s]",
Harald Weltec91085e2022-02-10 18:05:45 +010083 default=1,
84 )
Harald Welte3aa0b412024-03-14 16:22:23 +010085 parser.add_argument("-x", "--mcc", dest="mcc",
86 help="Mobile Country Code [default: %(default)s]",
Harald Weltec91085e2022-02-10 18:05:45 +010087 default="901",
88 )
Harald Welte3aa0b412024-03-14 16:22:23 +010089 parser.add_argument("-y", "--mnc", dest="mnc",
90 help="Mobile Network Code [default: %(default)s]",
Harald Weltec91085e2022-02-10 18:05:45 +010091 default="55",
92 )
Harald Welte3aa0b412024-03-14 16:22:23 +010093 parser.add_argument("--mnclen", dest="mnclen",
94 help="Length of Mobile Network Code [default: %(default)s]",
Philipp Maier32c04342022-12-16 16:39:24 +010095 default="auto",
96 choices=["2", "3", "auto"],
Harald Weltec91085e2022-02-10 18:05:45 +010097 )
Harald Welte3aa0b412024-03-14 16:22:23 +010098 parser.add_argument("-m", "--smsc", dest="smsc",
Harald Weltec91085e2022-02-10 18:05:45 +010099 help="SMSC number (Start with + for international no.) [default: '00 + country code + 5555']",
100 )
Harald Welte3aa0b412024-03-14 16:22:23 +0100101 parser.add_argument("-M", "--smsp", dest="smsp",
Harald Weltec91085e2022-02-10 18:05:45 +0100102 help="Raw SMSP content in hex [default: auto from SMSC]",
103 )
Sylvain Munaut76504e02010-12-07 00:24:32 +0100104
Harald Welte3aa0b412024-03-14 16:22:23 +0100105 parser.add_argument("-s", "--iccid", dest="iccid", metavar="ID",
Harald Weltec91085e2022-02-10 18:05:45 +0100106 help="Integrated Circuit Card ID",
107 )
Harald Welte3aa0b412024-03-14 16:22:23 +0100108 parser.add_argument("-i", "--imsi", dest="imsi",
Harald Weltec91085e2022-02-10 18:05:45 +0100109 help="International Mobile Subscriber Identity",
110 )
Harald Welte3aa0b412024-03-14 16:22:23 +0100111 parser.add_argument("--msisdn", dest="msisdn",
Harald Weltec91085e2022-02-10 18:05:45 +0100112 help="Mobile Subscriber Integrated Services Digital Number",
113 )
Harald Welte3aa0b412024-03-14 16:22:23 +0100114 parser.add_argument("-k", "--ki", dest="ki",
Harald Weltec91085e2022-02-10 18:05:45 +0100115 help="Ki (default is to randomize)",
116 )
Harald Welte3aa0b412024-03-14 16:22:23 +0100117 parser.add_argument("-o", "--opc", dest="opc",
Harald Weltec91085e2022-02-10 18:05:45 +0100118 help="OPC (default is to randomize)",
119 )
Harald Welte3aa0b412024-03-14 16:22:23 +0100120 parser.add_argument("--op", dest="op",
Harald Weltec91085e2022-02-10 18:05:45 +0100121 help="Set OP to derive OPC from OP and KI",
122 )
Harald Welte3aa0b412024-03-14 16:22:23 +0100123 parser.add_argument("--acc", dest="acc",
Harald Weltec91085e2022-02-10 18:05:45 +0100124 help="Set ACC bits (Access Control Code). not all card types are supported",
125 )
Harald Welte3aa0b412024-03-14 16:22:23 +0100126 parser.add_argument("--opmode", dest="opmode",
Harald Weltec91085e2022-02-10 18:05:45 +0100127 help="Set UE Operation Mode in EF.AD (Administrative Data)",
128 default=None,
129 choices=['{:02X}'.format(int(m)) for m in EF_AD.OP_MODE],
130 )
Harald Welte3aa0b412024-03-14 16:22:23 +0100131 parser.add_argument("-f", "--fplmn", dest="fplmn", action="append",
Matan Perelman777ee9e2023-05-14 08:58:50 +0300132 help="Set Forbidden PLMN. Add multiple time for multiple FPLMNS",
133 )
Harald Welte3aa0b412024-03-14 16:22:23 +0100134 parser.add_argument("--epdgid", dest="epdgid",
Harald Weltec91085e2022-02-10 18:05:45 +0100135 help="Set Home Evolved Packet Data Gateway (ePDG) Identifier. (Only FQDN format supported)",
136 )
Harald Welte3aa0b412024-03-14 16:22:23 +0100137 parser.add_argument("--epdgSelection", dest="epdgSelection",
Harald Weltec91085e2022-02-10 18:05:45 +0100138 help="Set PLMN for ePDG Selection Information. (Only Operator Identifier FQDN format supported)",
139 )
Harald Welte3aa0b412024-03-14 16:22:23 +0100140 parser.add_argument("--pcscf", dest="pcscf",
Harald Weltec91085e2022-02-10 18:05:45 +0100141 help="Set Proxy Call Session Control Function (P-CSCF) Address. (Only FQDN format supported)",
142 )
Harald Welte3aa0b412024-03-14 16:22:23 +0100143 parser.add_argument("--ims-hdomain", dest="ims_hdomain",
Harald Weltec91085e2022-02-10 18:05:45 +0100144 help="Set IMS Home Network Domain Name in FQDN format",
145 )
Harald Welte3aa0b412024-03-14 16:22:23 +0100146 parser.add_argument("--impi", dest="impi",
Harald Weltec91085e2022-02-10 18:05:45 +0100147 help="Set IMS private user identity",
148 )
Harald Welte3aa0b412024-03-14 16:22:23 +0100149 parser.add_argument("--impu", dest="impu",
Harald Weltec91085e2022-02-10 18:05:45 +0100150 help="Set IMS public user identity",
151 )
Harald Welte3aa0b412024-03-14 16:22:23 +0100152 parser.add_argument("--read-imsi", dest="read_imsi", action="store_true",
Harald Weltec91085e2022-02-10 18:05:45 +0100153 help="Read the IMSI from the CARD", default=False
154 )
Harald Welte3aa0b412024-03-14 16:22:23 +0100155 parser.add_argument("--read-iccid", dest="read_iccid", action="store_true",
Harald Weltec91085e2022-02-10 18:05:45 +0100156 help="Read the ICCID from the CARD", default=False
157 )
Harald Welte3aa0b412024-03-14 16:22:23 +0100158 parser.add_argument("-z", "--secret", dest="secret", metavar="STR",
Harald Weltec91085e2022-02-10 18:05:45 +0100159 help="Secret used for ICCID/IMSI autogen",
160 )
Harald Welte3aa0b412024-03-14 16:22:23 +0100161 parser.add_argument("-j", "--num", dest="num", type=int,
Harald Weltec91085e2022-02-10 18:05:45 +0100162 help="Card # used for ICCID/IMSI autogen",
163 )
Harald Welte3aa0b412024-03-14 16:22:23 +0100164 parser.add_argument("--batch", dest="batch_mode",
165 help="Enable batch mode [default: %(default)s]",
Harald Weltec91085e2022-02-10 18:05:45 +0100166 default=False, action='store_true',
167 )
Harald Welte3aa0b412024-03-14 16:22:23 +0100168 parser.add_argument("--batch-state", dest="batch_state", metavar="FILE",
Harald Weltec91085e2022-02-10 18:05:45 +0100169 help="Optional batch state file",
170 )
Sylvain Munaut76504e02010-12-07 00:24:32 +0100171
Harald Weltec91085e2022-02-10 18:05:45 +0100172 # if mode is "csv"
Harald Welte3aa0b412024-03-14 16:22:23 +0100173 parser.add_argument("--read-csv", dest="read_csv", metavar="FILE",
Harald Weltec91085e2022-02-10 18:05:45 +0100174 help="Read parameters from CSV file rather than command line")
Harald Welte7f62cec2012-08-13 20:07:41 +0200175
Harald Welte3aa0b412024-03-14 16:22:23 +0100176 parser.add_argument("--write-csv", dest="write_csv", metavar="FILE",
Harald Weltec91085e2022-02-10 18:05:45 +0100177 help="Append generated parameters in CSV file",
178 )
Harald Welte3aa0b412024-03-14 16:22:23 +0100179 parser.add_argument("--write-hlr", dest="write_hlr", metavar="FILE",
Harald Weltec91085e2022-02-10 18:05:45 +0100180 help="Append generated parameters to OpenBSC HLR sqlite3",
181 )
Harald Welte3aa0b412024-03-14 16:22:23 +0100182 parser.add_argument("--dry-run", dest="dry_run",
Harald Weltec91085e2022-02-10 18:05:45 +0100183 help="Perform a 'dry run', don't actually program the card",
184 default=False, action="store_true")
Harald Welte3aa0b412024-03-14 16:22:23 +0100185 parser.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
Harald Weltec91085e2022-02-10 18:05:45 +0100186 help="Use automatic card handling machine")
Harald Welte7f62cec2012-08-13 20:07:41 +0200187
Harald Welte3aa0b412024-03-14 16:22:23 +0100188 options = parser.parse_args()
Philipp Maierc5b422e2019-08-30 11:41:02 +0200189
Harald Weltec91085e2022-02-10 18:05:45 +0100190 if options.type == 'list':
191 for kls in _cards_classes:
192 print(kls.name)
193 sys.exit(0)
Sylvain Munaut76504e02010-12-07 00:24:32 +0100194
Harald Weltec91085e2022-02-10 18:05:45 +0100195 if options.probe:
196 return options
Sylvain Munaut76504e02010-12-07 00:24:32 +0100197
Harald Weltec91085e2022-02-10 18:05:45 +0100198 if options.source == 'csv':
199 if (options.imsi is None) and (options.batch_mode is False) and (options.read_imsi is False) and (options.read_iccid is False):
200 parser.error(
201 "CSV mode needs either an IMSI, --read-imsi, --read-iccid or batch mode")
202 if options.read_csv is None:
203 parser.error("CSV mode requires a CSV input file")
204 elif options.source == 'cmdline':
205 if ((options.imsi is None) or (options.iccid is None)) and (options.num is None):
206 parser.error(
207 "If either IMSI or ICCID isn't specified, num is required")
208 else:
209 parser.error("Only `cmdline' and `csv' sources supported")
Philipp Maierac9dde62018-07-04 11:05:14 +0200210
Harald Weltec91085e2022-02-10 18:05:45 +0100211 if (options.read_csv is not None) and (options.source != 'csv'):
212 parser.error("You cannot specify a CSV input file in source != csv")
Harald Welte7f62cec2012-08-13 20:07:41 +0200213
Harald Weltec91085e2022-02-10 18:05:45 +0100214 if (options.batch_mode) and (options.num is None):
215 options.num = 0
Harald Welte7f62cec2012-08-13 20:07:41 +0200216
Harald Weltec91085e2022-02-10 18:05:45 +0100217 if (options.batch_mode):
218 if (options.imsi is not None) or (options.iccid is not None):
219 parser.error(
220 "Can't give ICCID/IMSI for batch mode, need to use automatic parameters ! see --num and --secret for more information")
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100221
Harald Weltec91085e2022-02-10 18:05:45 +0100222 return options
Sylvain Munaut76504e02010-12-07 00:24:32 +0100223
224
225def _digits(secret, usage, len, num):
Harald Weltec91085e2022-02-10 18:05:45 +0100226 seed = secret + usage + '%d' % num
227 s = hashlib.sha1(seed.encode())
228 d = ''.join(['%02d' % x for x in s.digest()])
229 return d[0:len]
230
Sylvain Munaut76504e02010-12-07 00:24:32 +0100231
232def _mcc_mnc_digits(mcc, mnc):
Harald Weltec91085e2022-02-10 18:05:45 +0100233 return '%s%s' % (mcc, mnc)
234
Sylvain Munaut76504e02010-12-07 00:24:32 +0100235
236def _cc_digits(cc):
Harald Weltec91085e2022-02-10 18:05:45 +0100237 return ('%03d' if cc > 100 else '%02d') % cc
238
Sylvain Munaut76504e02010-12-07 00:24:32 +0100239
240def _isnum(s, l=-1):
Harald Weltec91085e2022-02-10 18:05:45 +0100241 return s.isdigit() and ((l == -1) or (len(s) == l))
242
Sylvain Munaut76504e02010-12-07 00:24:32 +0100243
Sylvain Munaut607ce2a2011-12-08 20:16:43 +0100244def _ishex(s, l=-1):
Harald Weltec91085e2022-02-10 18:05:45 +0100245 hc = '0123456789abcdef'
246 return all([x in hc for x in s.lower()]) and ((l == -1) or (len(s) == l))
Sylvain Munaut607ce2a2011-12-08 20:16:43 +0100247
Sylvain Munaut76504e02010-12-07 00:24:32 +0100248
Sylvain Munaut9f120e02010-12-23 20:28:24 +0100249def _dbi_binary_quote(s):
Harald Weltec91085e2022-02-10 18:05:45 +0100250 # Count usage of each char
251 cnt = {}
252 for c in s:
253 cnt[c] = cnt.get(c, 0) + 1
Sylvain Munaut9f120e02010-12-23 20:28:24 +0100254
Harald Weltec91085e2022-02-10 18:05:45 +0100255 # Find best offset
256 e = 0
257 m = len(s)
258 for i in range(1, 256):
259 if i == 39:
260 continue
261 sum_ = cnt.get(i, 0) + cnt.get((i+1) & 0xff, 0) + \
262 cnt.get((i+39) & 0xff, 0)
263 if sum_ < m:
264 m = sum_
265 e = i
266 if m == 0: # No overhead ? use this !
267 break
Sylvain Munaut1a914432011-12-08 20:08:26 +0100268
Harald Weltec91085e2022-02-10 18:05:45 +0100269 # Generate output
270 out = []
271 out.append(chr(e)) # Offset
272 for c in s:
273 x = (256 + ord(c) - e) % 256
274 if x in (0, 1, 39):
275 out.append('\x01')
276 out.append(chr(x+1))
277 else:
278 out.append(chr(x))
Sylvain Munaut9f120e02010-12-23 20:28:24 +0100279
Harald Weltec91085e2022-02-10 18:05:45 +0100280 return ''.join(out)
281
Sylvain Munaut9f120e02010-12-23 20:28:24 +0100282
Sylvain Munaut76504e02010-12-07 00:24:32 +0100283def gen_parameters(opts):
Harald Weltec91085e2022-02-10 18:05:45 +0100284 """Generates Name, ICCID, MCC, MNC, IMSI, SMSP, Ki, PIN-ADM from the
285 options given by the user"""
Sylvain Munaut76504e02010-12-07 00:24:32 +0100286
Harald Weltec91085e2022-02-10 18:05:45 +0100287 # MCC/MNC
288 mcc = opts.mcc
289 mnc = opts.mnc
Sylvain Munaut76504e02010-12-07 00:24:32 +0100290
Harald Weltec91085e2022-02-10 18:05:45 +0100291 if not mcc.isdigit() or not mnc.isdigit():
292 raise ValueError('mcc & mnc must only contain decimal digits')
293 if len(mcc) < 1 or len(mcc) > 3:
294 raise ValueError('mcc must be between 1 .. 3 digits')
295 if len(mnc) < 1 or len(mnc) > 3:
296 raise ValueError('mnc must be between 1 .. 3 digits')
Harald Welte7f1d3c42020-05-12 21:12:44 +0200297
Harald Weltec91085e2022-02-10 18:05:45 +0100298 # MCC always has 3 digits
299 mcc = lpad(mcc, 3, "0")
Philipp Maier32c04342022-12-16 16:39:24 +0100300
301 # The MNC must be at least 2 digits long. This is also the most common case.
302 # The user may specify an explicit length using the --mnclen option.
303 if opts.mnclen != "auto":
304 if len(mnc) > int(opts.mnclen):
305 raise ValueError('mcc is longer than specified in option --mnclen')
306 mnc = lpad(mnc, int(opts.mnclen), "0")
307 else:
308 mnc = lpad(mnc, 2, "0")
Sylvain Munaut76504e02010-12-07 00:24:32 +0100309
Harald Weltec91085e2022-02-10 18:05:45 +0100310 # Digitize country code (2 or 3 digits)
311 cc_digits = _cc_digits(opts.country)
Sylvain Munaut76504e02010-12-07 00:24:32 +0100312
Harald Weltec91085e2022-02-10 18:05:45 +0100313 # Digitize MCC/MNC (5 or 6 digits)
314 plmn_digits = _mcc_mnc_digits(mcc, mnc)
Sylvain Munaut76504e02010-12-07 00:24:32 +0100315
Harald Weltec91085e2022-02-10 18:05:45 +0100316 if opts.name is not None:
317 if len(opts.name) > 16:
318 raise ValueError('Service Provider Name must max 16 characters!')
Supreeth Herle840a9e22020-01-21 13:32:46 +0100319
Harald Weltec91085e2022-02-10 18:05:45 +0100320 if opts.msisdn is not None:
321 msisdn = opts.msisdn
322 if msisdn[0] == '+':
323 msisdn = msisdn[1:]
324 if not msisdn.isdigit():
325 raise ValueError('MSISDN must be digits only! '
326 'Start with \'+\' for international numbers.')
327 if len(msisdn) > 10 * 2:
328 # TODO: Support MSISDN of length > 20 (10 Bytes)
329 raise ValueError(
330 'MSISDNs longer than 20 digits are not (yet) supported.')
Supreeth Herle5a541012019-12-22 08:59:16 +0100331
Harald Weltec91085e2022-02-10 18:05:45 +0100332 # ICCID (19 digits, E.118), though some phase1 vendors use 20 :(
333 if opts.iccid is not None:
334 iccid = opts.iccid
Harald Welte284efda2023-07-11 11:08:24 +0200335 if not _isnum(iccid, 18) and not _isnum(iccid, 19) and not _isnum(iccid, 20):
336 raise ValueError('ICCID must be 18, 19 or 20 digits !')
Sylvain Munaut76504e02010-12-07 00:24:32 +0100337
Harald Weltec91085e2022-02-10 18:05:45 +0100338 else:
339 if opts.num is None:
340 raise ValueError('Neither ICCID nor card number specified !')
Sylvain Munaut76504e02010-12-07 00:24:32 +0100341
Harald Weltec91085e2022-02-10 18:05:45 +0100342 iccid = (
343 '89' + # Common prefix (telecom)
344 cc_digits + # Country Code on 2/3 digits
345 plmn_digits # MCC/MNC on 5/6 digits
346 )
Sylvain Munaut76504e02010-12-07 00:24:32 +0100347
Harald Weltec91085e2022-02-10 18:05:45 +0100348 ml = 18 - len(iccid)
Sylvain Munaut76504e02010-12-07 00:24:32 +0100349
Harald Weltec91085e2022-02-10 18:05:45 +0100350 if opts.secret is None:
351 # The raw number
352 iccid += ('%%0%dd' % ml) % opts.num
353 else:
354 # Randomized digits
355 iccid += _digits(opts.secret, 'ccid', ml, opts.num)
Sylvain Munaut76504e02010-12-07 00:24:32 +0100356
Harald Weltec91085e2022-02-10 18:05:45 +0100357 # Add checksum digit
358 iccid += ('%1d' % calculate_luhn(iccid))
Harald Welte2c0ff3a2011-12-07 12:34:13 +0100359
Harald Weltec91085e2022-02-10 18:05:45 +0100360 # IMSI (15 digits usually)
361 if opts.imsi is not None:
362 imsi = opts.imsi
363 if not _isnum(imsi):
364 raise ValueError('IMSI must be digits only !')
Sylvain Munaut76504e02010-12-07 00:24:32 +0100365
Harald Weltec91085e2022-02-10 18:05:45 +0100366 else:
367 if opts.num is None:
368 raise ValueError('Neither IMSI nor card number specified !')
Sylvain Munaut76504e02010-12-07 00:24:32 +0100369
Harald Weltec91085e2022-02-10 18:05:45 +0100370 ml = 15 - len(plmn_digits)
Sylvain Munaut76504e02010-12-07 00:24:32 +0100371
Harald Weltec91085e2022-02-10 18:05:45 +0100372 if opts.secret is None:
373 # The raw number
374 msin = ('%%0%dd' % ml) % opts.num
375 else:
376 # Randomized digits
377 msin = _digits(opts.secret, 'imsi', ml, opts.num)
Sylvain Munaut76504e02010-12-07 00:24:32 +0100378
Harald Weltec91085e2022-02-10 18:05:45 +0100379 imsi = (
380 plmn_digits + # MCC/MNC on 5/6 digits
381 msin # MSIN
382 )
Sylvain Munaut76504e02010-12-07 00:24:32 +0100383
Harald Weltec91085e2022-02-10 18:05:45 +0100384 # SMSP
385 if opts.smsp is not None:
386 smsp = opts.smsp
387 if not _ishex(smsp):
388 raise ValueError('SMSP must be hex digits only !')
389 if len(smsp) < 28*2:
390 raise ValueError('SMSP must be at least 28 bytes')
Sylvain Munaut76504e02010-12-07 00:24:32 +0100391
Harald Weltec91085e2022-02-10 18:05:45 +0100392 else:
393 ton = "81"
394 if opts.smsc is not None:
395 smsc = opts.smsc
396 if smsc[0] == '+':
397 ton = "91"
398 smsc = smsc[1:]
399 if not _isnum(smsc):
400 raise ValueError('SMSC must be digits only!\n \
Daniel Willmann4fa8f1c2018-10-02 18:10:21 +0200401 Start with \'+\' for international numbers')
Harald Weltec91085e2022-02-10 18:05:45 +0100402 else:
403 smsc = '00%d' % opts.country + '5555' # Hack ...
Sylvain Munaut607ce2a2011-12-08 20:16:43 +0100404
Harald Weltec91085e2022-02-10 18:05:45 +0100405 smsc = '%02d' % ((len(smsc) + 3)//2,) + ton + \
406 swap_nibbles(rpad(smsc, 20))
Sylvain Munaut607ce2a2011-12-08 20:16:43 +0100407
Harald Weltec91085e2022-02-10 18:05:45 +0100408 smsp = (
409 'e1' + # Parameters indicator
410 'ff' * 12 + # TP-Destination address
411 smsc + # TP-Service Centre Address
412 '00' + # TP-Protocol identifier
413 '00' + # TP-Data coding scheme
414 '00' # TP-Validity period
415 )
Sylvain Munaut76504e02010-12-07 00:24:32 +0100416
Harald Weltec91085e2022-02-10 18:05:45 +0100417 # ACC
418 if opts.acc is not None:
419 acc = opts.acc
420 if not _ishex(acc):
421 raise ValueError('ACC must be hex digits only !')
422 if len(acc) != 2*2:
423 raise ValueError('ACC must be exactly 2 bytes')
Alexander Chemeris21885242013-07-02 16:56:55 +0400424
Harald Weltec91085e2022-02-10 18:05:45 +0100425 else:
426 acc = None
Alexander Chemeris21885242013-07-02 16:56:55 +0400427
Harald Weltec91085e2022-02-10 18:05:45 +0100428 # Ki (random)
429 if opts.ki is not None:
430 ki = opts.ki
431 if not re.match('^[0-9a-fA-F]{32}$', ki):
432 raise ValueError('Ki needs to be 128 bits, in hex format')
433 else:
434 ki = ''.join(['%02x' % random.randrange(0, 256) for i in range(16)])
Sylvain Munaut76504e02010-12-07 00:24:32 +0100435
Harald Weltec91085e2022-02-10 18:05:45 +0100436 # OPC (random)
437 if opts.opc is not None:
438 opc = opts.opc
439 if not re.match('^[0-9a-fA-F]{32}$', opc):
440 raise ValueError('OPC needs to be 128 bits, in hex format')
Harald Welte93b38cd2012-03-22 14:31:36 +0100441
Harald Weltec91085e2022-02-10 18:05:45 +0100442 elif opts.op is not None:
443 opc = derive_milenage_opc(ki, opts.op)
444 else:
445 opc = ''.join(['%02x' % random.randrange(0, 256) for i in range(16)])
Harald Welte93b38cd2012-03-22 14:31:36 +0100446
Harald Weltec91085e2022-02-10 18:05:45 +0100447 pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
Harald Welte93b38cd2012-03-22 14:31:36 +0100448
Harald Weltec91085e2022-02-10 18:05:45 +0100449 # ePDG Selection Information
450 if opts.epdgSelection:
451 if len(opts.epdgSelection) < 5 or len(opts.epdgSelection) > 6:
452 raise ValueError('ePDG Selection Information is not valid')
453 epdg_mcc = opts.epdgSelection[:3]
454 epdg_mnc = opts.epdgSelection[3:]
455 if not epdg_mcc.isdigit() or not epdg_mnc.isdigit():
456 raise ValueError(
457 'PLMN for ePDG Selection must only contain decimal digits')
Supreeth Herlef964df42020-03-24 13:15:37 +0100458
Harald Weltec91085e2022-02-10 18:05:45 +0100459 # Return that
460 return {
461 'name': opts.name,
462 'iccid': iccid,
463 'mcc': mcc,
464 'mnc': mnc,
465 'imsi': imsi,
466 'smsp': smsp,
467 'ki': ki,
468 'opc': opc,
469 'acc': acc,
470 'pin_adm': pin_adm,
471 'msisdn': opts.msisdn,
472 'epdgid': opts.epdgid,
473 'epdgSelection': opts.epdgSelection,
474 'pcscf': opts.pcscf,
475 'ims_hdomain': opts.ims_hdomain,
476 'impi': opts.impi,
477 'impu': opts.impu,
478 'opmode': opts.opmode,
Matan Perelman777ee9e2023-05-14 08:58:50 +0300479 'fplmn': opts.fplmn,
Harald Weltec91085e2022-02-10 18:05:45 +0100480 }
Sylvain Munaut76504e02010-12-07 00:24:32 +0100481
482
483def print_parameters(params):
484
Harald Weltec91085e2022-02-10 18:05:45 +0100485 s = ["Generated card parameters :"]
486 if 'name' in params:
487 s.append(" > Name : %(name)s")
488 if 'smsp' in params:
489 s.append(" > SMSP : %(smsp)s")
490 s.append(" > ICCID : %(iccid)s")
491 s.append(" > MCC/MNC : %(mcc)s/%(mnc)s")
492 s.append(" > IMSI : %(imsi)s")
493 s.append(" > Ki : %(ki)s")
494 s.append(" > OPC : %(opc)s")
495 if 'acc' in params:
496 s.append(" > ACC : %(acc)s")
497 s.append(" > ADM1(hex): %(pin_adm)s")
498 if 'opmode' in params:
499 s.append(" > OPMODE : %(opmode)s")
500 print("\n".join(s) % params)
Sylvain Munaut76504e02010-12-07 00:24:32 +0100501
502
Harald Welte130524b2012-08-13 15:53:43 +0200503def write_params_csv(opts, params):
Harald Weltec91085e2022-02-10 18:05:45 +0100504 # csv
505 if opts.write_csv:
506 import csv
507 row = ['name', 'iccid', 'mcc', 'mnc', 'imsi', 'smsp', 'ki', 'opc']
508 f = open(opts.write_csv, 'a')
509 cw = csv.writer(f)
510 cw.writerow([params[x] for x in row])
511 f.close()
512
Sylvain Munaut143e99d2010-12-08 22:35:04 +0100513
Philipp Maier2688ddf2022-12-16 13:36:42 +0100514def find_row_in_csv_file(csv_file_name:str, num=None, iccid=None, imsi=None):
515 """
516 Pick a matching row in a CSV file by row number or ICCID or IMSI. When num
517 is not None, the search parameters iccid and imsi are ignored. When
518 searching for a specific ICCID or IMSI the caller must set num to None. It
519 is possible to search for an ICCID or an IMSI at the same time. The first
520 line that either contains a matching ICCID or IMSI is returned. Unused
521 search parameters must be set to None.
522 """
523
524 f = open(csv_file_name, 'r')
Harald Weltec91085e2022-02-10 18:05:45 +0100525 cr = csv.DictReader(f)
Philipp Maier120a0002019-09-12 13:11:45 +0200526
Philipp Maier2688ddf2022-12-16 13:36:42 +0100527 # Make sure the CSV file contains at least the fields we are searching for
528 if not 'iccid' in cr.fieldnames:
529 raise Exception("wrong CSV file format - no field \"iccid\" or missing header!")
530 if not 'imsi' in cr.fieldnames:
531 raise Exception("wrong CSV file format - no field \"imsi\" or missing header!")
532
533 # Enforce at least one search parameter
534 if not num and not iccid and not imsi:
535 raise Exception("no CSV file search parameters!")
536
Harald Weltec91085e2022-02-10 18:05:45 +0100537 # Lower-case fieldnames
538 cr.fieldnames = [field.lower() for field in cr.fieldnames]
Philipp Maier120a0002019-09-12 13:11:45 +0200539
Harald Weltec91085e2022-02-10 18:05:45 +0100540 i = 0
Harald Weltec91085e2022-02-10 18:05:45 +0100541 for row in cr:
Philipp Maier2688ddf2022-12-16 13:36:42 +0100542 # Pick a specific row by line number (num)
543 if num is not None and iccid is None and imsi is None:
Philipp Maier91c971b2023-10-09 10:48:46 +0200544 if num == i:
Harald Weltec91085e2022-02-10 18:05:45 +0100545 f.close()
546 return row
Philipp Maier2688ddf2022-12-16 13:36:42 +0100547
548 # Pick the first row that contains the specified ICCID
Harald Weltec91085e2022-02-10 18:05:45 +0100549 if row['iccid'] == iccid:
550 f.close()
551 return row
Daniel Willmann164b9632019-09-03 19:13:51 +0200552
Philipp Maier2688ddf2022-12-16 13:36:42 +0100553 # Pick the first row that contains the specified IMSI
Harald Weltec91085e2022-02-10 18:05:45 +0100554 if row['imsi'] == imsi:
555 f.close()
556 return row
Harald Welte7f62cec2012-08-13 20:07:41 +0200557
Philipp Maier2688ddf2022-12-16 13:36:42 +0100558 i += 1
559
Harald Weltec91085e2022-02-10 18:05:45 +0100560 f.close()
Philipp Maier2688ddf2022-12-16 13:36:42 +0100561 print("Could not read card parameters from CSV file, no matching entry found.")
Harald Weltec91085e2022-02-10 18:05:45 +0100562 return None
563
Harald Weltec26b8292012-08-15 15:25:51 +0200564
Daniel Willmann164b9632019-09-03 19:13:51 +0200565def read_params_csv(opts, imsi=None, iccid=None):
Philipp Maier4237ccf2022-12-20 11:21:47 +0100566 """
567 Read the card parameters from a CSV file. This function will generate the
568 same dictionary that gen_parameters would generate from parameters passed as
569 commandline arguments.
570 """
571
Philipp Maier2688ddf2022-12-16 13:36:42 +0100572 row = find_row_in_csv_file(opts.read_csv, opts.num, iccid=iccid, imsi=imsi)
Harald Weltec91085e2022-02-10 18:05:45 +0100573 if row is not None:
574 row['mcc'] = row.get('mcc', mcc_from_imsi(row.get('imsi')))
Philipp Maier32c04342022-12-16 16:39:24 +0100575
576 # We cannot determine the MNC length (2 or 3 digits?) from the IMSI
577 # alone. In cases where the user has specified an mnclen via the
578 # commandline options we can use that info, otherwise we guess that
579 # the length is 2, which is also the most common case.
580 if opts.mnclen != "auto":
Vadim Yanitskiy0d80fa92023-04-14 00:11:19 +0700581 if opts.mnclen == "2":
Philipp Maier32c04342022-12-16 16:39:24 +0100582 row['mnc'] = row.get('mnc', mnc_from_imsi(row.get('imsi'), False))
Vadim Yanitskiy0d80fa92023-04-14 00:11:19 +0700583 elif opts.mnclen == "3":
Philipp Maier32c04342022-12-16 16:39:24 +0100584 row['mnc'] = row.get('mnc', mnc_from_imsi(row.get('imsi'), True))
585 else:
586 raise ValueError("invalid parameter --mnclen, must be 2 or 3 or auto")
587 else:
588 row['mnc'] = row.get('mnc', mnc_from_imsi(row.get('imsi'), False))
589
590 # NOTE: We might concider to specify a new CSV field "mnclen" in our
591 # CSV files for a better automatization. However, this only makes sense
592 # when the tools and databases we export our files from will also add
593 # such a field.
Philipp Maier7592eee2019-09-12 13:03:23 +0200594
Harald Weltec91085e2022-02-10 18:05:45 +0100595 pin_adm = None
596 # We need to escape the pin_adm we get from the csv
597 if 'pin_adm' in row:
598 pin_adm = ''.join(['%02x' % (ord(x)) for x in row['pin_adm']])
599 # Stay compatible to the odoo csv format
600 elif 'adm1' in row:
601 pin_adm = ''.join(['%02x' % (ord(x)) for x in row['adm1']])
602 if pin_adm:
603 row['pin_adm'] = rpad(pin_adm, 16)
Philipp Maiere053da52019-09-05 13:08:36 +0200604
Harald Weltec91085e2022-02-10 18:05:45 +0100605 # If the CSV-File defines a pin_adm_hex field use this field to
606 # generate pin_adm from that.
607 pin_adm_hex = row.get('pin_adm_hex')
608 if pin_adm_hex:
609 if len(pin_adm_hex) == 16:
610 row['pin_adm'] = pin_adm_hex
611 # Ensure that it's hex-encoded
612 try:
613 try_encode = h2b(pin_adm)
614 except ValueError:
615 raise ValueError(
616 "pin_adm_hex needs to be hex encoded using this option")
617 else:
618 raise ValueError(
619 "pin_adm_hex needs to be exactly 16 digits (hex encoded)")
Philipp Maiere053da52019-09-05 13:08:36 +0200620
Harald Weltec91085e2022-02-10 18:05:45 +0100621 return row
Harald Welte7f62cec2012-08-13 20:07:41 +0200622
Harald Weltec26b8292012-08-15 15:25:51 +0200623
Harald Welte130524b2012-08-13 15:53:43 +0200624def write_params_hlr(opts, params):
Harald Weltec91085e2022-02-10 18:05:45 +0100625 # SQLite3 OpenBSC HLR
626 if opts.write_hlr:
627 import sqlite3
628 conn = sqlite3.connect(opts.write_hlr)
Sylvain Munaut143e99d2010-12-08 22:35:04 +0100629
Harald Weltec91085e2022-02-10 18:05:45 +0100630 c = conn.execute(
631 'INSERT INTO Subscriber ' +
632 '(imsi, name, extension, authorized, created, updated) ' +
633 'VALUES ' +
634 '(?,?,?,1,datetime(\'now\'),datetime(\'now\'));',
635 [
636 params['imsi'],
637 params['name'],
638 '9' + params['iccid'][-5:-1]
639 ],
640 )
641 sub_id = c.lastrowid
642 c.close()
Sylvain Munaut143e99d2010-12-08 22:35:04 +0100643
Harald Weltec91085e2022-02-10 18:05:45 +0100644 c = conn.execute(
645 'INSERT INTO AuthKeys ' +
646 '(subscriber_id, algorithm_id, a3a8_ki)' +
647 'VALUES ' +
648 '(?,?,?)',
649 [sub_id, 2, sqlite3.Binary(
650 _dbi_binary_quote(h2b(params['ki'])))],
651 )
Sylvain Munaut143e99d2010-12-08 22:35:04 +0100652
Harald Weltec91085e2022-02-10 18:05:45 +0100653 conn.commit()
654 conn.close()
655
Sylvain Munaut143e99d2010-12-08 22:35:04 +0100656
Philipp Maier5f0cb3c2022-12-16 17:03:01 +0100657def write_parameters_to_csv_and_hlr(opts, params):
Harald Weltec91085e2022-02-10 18:05:45 +0100658 write_params_csv(opts, params)
659 write_params_hlr(opts, params)
Harald Welte130524b2012-08-13 15:53:43 +0200660
Sylvain Munaut143e99d2010-12-08 22:35:04 +0100661
Harald Weltec91085e2022-02-10 18:05:45 +0100662BATCH_STATE = ['name', 'country', 'mcc', 'mnc', 'smsp', 'secret', 'num']
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100663BATCH_INCOMPATIBLE = ['iccid', 'imsi', 'ki']
Sylvain Munaut143e99d2010-12-08 22:35:04 +0100664
Harald Weltec91085e2022-02-10 18:05:45 +0100665
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100666def init_batch(opts):
Harald Weltec91085e2022-02-10 18:05:45 +0100667 # Need to do something ?
668 if not opts.batch_mode:
669 return
Sylvain Munaut76504e02010-12-07 00:24:32 +0100670
Harald Weltec91085e2022-02-10 18:05:45 +0100671 for k in BATCH_INCOMPATIBLE:
672 if getattr(opts, k):
673 print("Incompatible option with batch_state: %s" % (k,))
674 sys.exit(-1)
Sylvain Munaut76504e02010-12-07 00:24:32 +0100675
Harald Weltec91085e2022-02-10 18:05:45 +0100676 # Don't load state if there is none ...
677 if not opts.batch_state:
678 return
Sylvain Munaut76504e02010-12-07 00:24:32 +0100679
Harald Weltec91085e2022-02-10 18:05:45 +0100680 if not os.path.isfile(opts.batch_state):
681 print("No state file yet")
682 return
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100683
Harald Weltec91085e2022-02-10 18:05:45 +0100684 # Get stored data
685 fh = open(opts.batch_state)
686 d = json.loads(fh.read())
687 fh.close()
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100688
Harald Weltec91085e2022-02-10 18:05:45 +0100689 for k, v in d.iteritems():
690 setattr(opts, k, v)
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100691
692
693def save_batch(opts):
Harald Weltec91085e2022-02-10 18:05:45 +0100694 # Need to do something ?
695 if not opts.batch_mode or not opts.batch_state:
696 return
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100697
Harald Weltec91085e2022-02-10 18:05:45 +0100698 d = json.dumps(dict([(k, getattr(opts, k)) for k in BATCH_STATE]))
699 fh = open(opts.batch_state, 'w')
700 fh.write(d)
701 fh.close()
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100702
703
Philipp Maier91c971b2023-10-09 10:48:46 +0200704def process_card(scc, opts, first, ch):
Philipp Maierc5b422e2019-08-30 11:41:02 +0200705
Philipp Maiercbb8c022022-12-16 16:57:16 +0100706 # Connect transport
707 ch.get(first)
708
709 # Get card
710 card = card_detect(opts.type, scc)
711 if card is None:
712 print("No card detected!")
713 return -1
714
715 # Probe only
716 if opts.probe:
717 return 0
718
719 # Erase if requested (not in dry run mode!)
Harald Weltec91085e2022-02-10 18:05:45 +0100720 if opts.dry_run is False:
Harald Weltec91085e2022-02-10 18:05:45 +0100721 if opts.erase:
722 print("Formatting ...")
723 card.erase()
724 card.reset()
Philipp Maierc5b422e2019-08-30 11:41:02 +0200725
Harald Weltea3f22ea2024-05-22 18:02:16 +0200726 cp = None
Harald Weltec91085e2022-02-10 18:05:45 +0100727 # Generate parameters
728 if opts.source == 'cmdline':
729 cp = gen_parameters(opts)
730 elif opts.source == 'csv':
731 imsi = None
732 iccid = None
733 if opts.read_iccid:
Harald Weltec91085e2022-02-10 18:05:45 +0100734 (res, _) = scc.read_binary(['3f00', '2fe2'], length=10)
735 iccid = dec_iccid(res)
736 elif opts.read_imsi:
Harald Weltec91085e2022-02-10 18:05:45 +0100737 (res, _) = scc.read_binary(EF['IMSI'])
738 imsi = swap_nibbles(res)[3:]
739 else:
740 imsi = opts.imsi
741 cp = read_params_csv(opts, imsi=imsi, iccid=iccid)
742 if cp is None:
Harald Weltec91085e2022-02-10 18:05:45 +0100743 return 2
744 print_parameters(cp)
Philipp Maierc5b422e2019-08-30 11:41:02 +0200745
Harald Weltec91085e2022-02-10 18:05:45 +0100746 if opts.dry_run is False:
747 # Program the card
748 print("Programming ...")
749 card.program(cp)
750 else:
751 print("Dry Run: NOT PROGRAMMING!")
Philipp Maierc5b422e2019-08-30 11:41:02 +0200752
Philipp Maier5f0cb3c2022-12-16 17:03:01 +0100753 # Write parameters to a specified CSV file or an HLR database (not the card)
754 write_parameters_to_csv_and_hlr(opts, cp)
Philipp Maierc5b422e2019-08-30 11:41:02 +0200755
Harald Weltec91085e2022-02-10 18:05:45 +0100756 # Batch mode state update and save
757 if opts.num is not None:
758 opts.num += 1
759 save_batch(opts)
Philipp Maierc5b422e2019-08-30 11:41:02 +0200760
Harald Weltec91085e2022-02-10 18:05:45 +0100761 ch.done()
762 return 0
Philipp Maierc5b422e2019-08-30 11:41:02 +0200763
764
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100765if __name__ == '__main__':
766
Harald Weltec91085e2022-02-10 18:05:45 +0100767 # Parse options
768 opts = parse_options()
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100769
Harald Weltec91085e2022-02-10 18:05:45 +0100770 # Init card reader driver
771 sl = init_reader(opts)
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100772
Harald Weltec91085e2022-02-10 18:05:45 +0100773 # Create command layer
774 scc = SimCardCommands(transport=sl)
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100775
Harald Weltec91085e2022-02-10 18:05:45 +0100776 # If we use a CSV file as data input, check if the CSV file exists.
777 if opts.source == 'csv':
778 print("Using CSV file as data input: " + str(opts.read_csv))
779 if not os.path.isfile(opts.read_csv):
780 print("CSV file not found!")
781 sys.exit(1)
Philipp Maier196b08c2019-09-12 11:49:44 +0200782
Harald Weltec91085e2022-02-10 18:05:45 +0100783 # Batch mode init
784 init_batch(opts)
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100785
Harald Weltec91085e2022-02-10 18:05:45 +0100786 if opts.card_handler_config:
787 ch = CardHandlerAuto(sl, opts.card_handler_config)
788 else:
789 ch = CardHandler(sl)
Philipp Maierc5b422e2019-08-30 11:41:02 +0200790
Harald Weltec91085e2022-02-10 18:05:45 +0100791 # Iterate
792 first = True
793 card = None
Sylvain Munaut1a914432011-12-08 20:08:26 +0100794
Harald Weltec91085e2022-02-10 18:05:45 +0100795 while 1:
796 try:
Philipp Maier91c971b2023-10-09 10:48:46 +0200797 rc = process_card(scc, opts, first, ch)
Harald Weltec91085e2022-02-10 18:05:45 +0100798 except (KeyboardInterrupt):
799 print("")
800 print("Terminated by user!")
801 sys.exit(0)
802 except (SystemExit):
803 raise
804 except:
805 print("")
806 print("Card programming failed with an exception:")
807 print("---------------------8<---------------------")
808 traceback.print_exc()
809 print("---------------------8<---------------------")
810 print("")
811 rc = -1
Harald Weltee9e5ecb2012-08-15 15:26:30 +0200812
Harald Weltec91085e2022-02-10 18:05:45 +0100813 # Something did not work as well as expected, however, lets
814 # make sure the card is pulled from the reader.
815 if rc != 0:
816 ch.error()
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100817
Harald Weltec91085e2022-02-10 18:05:45 +0100818 # If we are not in batch mode we are done in any case, so lets
819 # exit here.
820 if not opts.batch_mode:
821 sys.exit(rc)
Philipp Maierc5b422e2019-08-30 11:41:02 +0200822
Harald Weltec91085e2022-02-10 18:05:45 +0100823 first = False