blob: 2911b464ff101b9edbfe5dcde2f66e0e3eca94a2 [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
28from optparse import OptionParser
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 Welte6e0458d2021-04-03 11:52:37 +020038from pySim.transport import init_reader
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 Weltec91085e2022-02-10 18:05:45 +010049 parser = OptionParser(usage="usage: %prog [options]")
Sylvain Munaut76504e02010-12-07 00:24:32 +010050
Harald Weltec91085e2022-02-10 18:05:45 +010051 parser.add_option("-d", "--device", dest="device", metavar="DEV",
52 help="Serial Device for SIM access [default: %default]",
53 default="/dev/ttyUSB0",
54 )
55 parser.add_option("-b", "--baud", dest="baudrate", type="int", metavar="BAUD",
56 help="Baudrate used for SIM access [default: %default]",
57 default=9600,
58 )
59 parser.add_option("-p", "--pcsc-device", dest="pcsc_dev", type='int', metavar="PCSC",
60 help="Which PC/SC reader number for SIM access",
61 default=None,
62 )
63 parser.add_option("--modem-device", dest="modem_dev", metavar="DEV",
64 help="Serial port of modem for Generic SIM Access (3GPP TS 27.007)",
65 default=None,
66 )
67 parser.add_option("--modem-baud", dest="modem_baud", type="int", metavar="BAUD",
68 help="Baudrate used for modem's port [default: %default]",
69 default=115200,
70 )
71 parser.add_option("--osmocon", dest="osmocon_sock", metavar="PATH",
72 help="Socket path for Calypso (e.g. Motorola C1XX) based reader (via OsmocomBB)",
73 default=None,
74 )
75 parser.add_option("-t", "--type", dest="type",
76 help="Card type (user -t list to view) [default: %default]",
77 default="auto",
78 )
79 parser.add_option("-T", "--probe", dest="probe",
80 help="Determine card type",
81 default=False, action="store_true"
82 )
83 parser.add_option("-a", "--pin-adm", dest="pin_adm",
84 help="ADM PIN used for provisioning (overwrites default)",
85 )
86 parser.add_option("-A", "--pin-adm-hex", dest="pin_adm_hex",
87 help="ADM PIN used for provisioning, as hex string (16 characters long",
88 )
89 parser.add_option("-e", "--erase", dest="erase", action='store_true',
90 help="Erase beforehand [default: %default]",
91 default=False,
92 )
Sylvain Munaut76504e02010-12-07 00:24:32 +010093
Harald Weltec91085e2022-02-10 18:05:45 +010094 parser.add_option("-S", "--source", dest="source",
95 help="Data Source[default: %default]",
96 default="cmdline",
97 )
Harald Welte7f62cec2012-08-13 20:07:41 +020098
Harald Weltec91085e2022-02-10 18:05:45 +010099 # if mode is "cmdline"
100 parser.add_option("-n", "--name", dest="name",
101 help="Operator name [default: %default]",
102 default="Magic",
103 )
104 parser.add_option("-c", "--country", dest="country", type="int", metavar="CC",
105 help="Country code [default: %default]",
106 default=1,
107 )
108 parser.add_option("-x", "--mcc", dest="mcc", type="string",
109 help="Mobile Country Code [default: %default]",
110 default="901",
111 )
112 parser.add_option("-y", "--mnc", dest="mnc", type="string",
113 help="Mobile Network Code [default: %default]",
114 default="55",
115 )
116 parser.add_option("--mnclen", dest="mnclen", type="choice",
117 help="Length of Mobile Network Code [default: %default]",
Philipp Maier32c04342022-12-16 16:39:24 +0100118 default="auto",
119 choices=["2", "3", "auto"],
Harald Weltec91085e2022-02-10 18:05:45 +0100120 )
121 parser.add_option("-m", "--smsc", dest="smsc",
122 help="SMSC number (Start with + for international no.) [default: '00 + country code + 5555']",
123 )
124 parser.add_option("-M", "--smsp", dest="smsp",
125 help="Raw SMSP content in hex [default: auto from SMSC]",
126 )
Sylvain Munaut76504e02010-12-07 00:24:32 +0100127
Harald Weltec91085e2022-02-10 18:05:45 +0100128 parser.add_option("-s", "--iccid", dest="iccid", metavar="ID",
129 help="Integrated Circuit Card ID",
130 )
131 parser.add_option("-i", "--imsi", dest="imsi",
132 help="International Mobile Subscriber Identity",
133 )
134 parser.add_option("--msisdn", dest="msisdn",
135 help="Mobile Subscriber Integrated Services Digital Number",
136 )
137 parser.add_option("-k", "--ki", dest="ki",
138 help="Ki (default is to randomize)",
139 )
140 parser.add_option("-o", "--opc", dest="opc",
141 help="OPC (default is to randomize)",
142 )
143 parser.add_option("--op", dest="op",
144 help="Set OP to derive OPC from OP and KI",
145 )
146 parser.add_option("--acc", dest="acc",
147 help="Set ACC bits (Access Control Code). not all card types are supported",
148 )
149 parser.add_option("--opmode", dest="opmode", type="choice",
150 help="Set UE Operation Mode in EF.AD (Administrative Data)",
151 default=None,
152 choices=['{:02X}'.format(int(m)) for m in EF_AD.OP_MODE],
153 )
Matan Perelman777ee9e2023-05-14 08:58:50 +0300154 parser.add_option("-f", "--fplmn", dest="fplmn", action="append",
155 help="Set Forbidden PLMN. Add multiple time for multiple FPLMNS",
156 )
Harald Weltec91085e2022-02-10 18:05:45 +0100157 parser.add_option("--epdgid", dest="epdgid",
158 help="Set Home Evolved Packet Data Gateway (ePDG) Identifier. (Only FQDN format supported)",
159 )
160 parser.add_option("--epdgSelection", dest="epdgSelection",
161 help="Set PLMN for ePDG Selection Information. (Only Operator Identifier FQDN format supported)",
162 )
163 parser.add_option("--pcscf", dest="pcscf",
164 help="Set Proxy Call Session Control Function (P-CSCF) Address. (Only FQDN format supported)",
165 )
166 parser.add_option("--ims-hdomain", dest="ims_hdomain",
167 help="Set IMS Home Network Domain Name in FQDN format",
168 )
169 parser.add_option("--impi", dest="impi",
170 help="Set IMS private user identity",
171 )
172 parser.add_option("--impu", dest="impu",
173 help="Set IMS public user identity",
174 )
175 parser.add_option("--read-imsi", dest="read_imsi", action="store_true",
176 help="Read the IMSI from the CARD", default=False
177 )
178 parser.add_option("--read-iccid", dest="read_iccid", action="store_true",
179 help="Read the ICCID from the CARD", default=False
180 )
181 parser.add_option("-z", "--secret", dest="secret", metavar="STR",
182 help="Secret used for ICCID/IMSI autogen",
183 )
184 parser.add_option("-j", "--num", dest="num", type=int,
185 help="Card # used for ICCID/IMSI autogen",
186 )
187 parser.add_option("--batch", dest="batch_mode",
188 help="Enable batch mode [default: %default]",
189 default=False, action='store_true',
190 )
191 parser.add_option("--batch-state", dest="batch_state", metavar="FILE",
192 help="Optional batch state file",
193 )
Sylvain Munaut76504e02010-12-07 00:24:32 +0100194
Harald Weltec91085e2022-02-10 18:05:45 +0100195 # if mode is "csv"
196 parser.add_option("--read-csv", dest="read_csv", metavar="FILE",
197 help="Read parameters from CSV file rather than command line")
Harald Welte7f62cec2012-08-13 20:07:41 +0200198
Harald Weltec91085e2022-02-10 18:05:45 +0100199 parser.add_option("--write-csv", dest="write_csv", metavar="FILE",
200 help="Append generated parameters in CSV file",
201 )
202 parser.add_option("--write-hlr", dest="write_hlr", metavar="FILE",
203 help="Append generated parameters to OpenBSC HLR sqlite3",
204 )
205 parser.add_option("--dry-run", dest="dry_run",
206 help="Perform a 'dry run', don't actually program the card",
207 default=False, action="store_true")
208 parser.add_option("--card_handler", dest="card_handler_config", metavar="FILE",
209 help="Use automatic card handling machine")
Harald Welte7f62cec2012-08-13 20:07:41 +0200210
Harald Weltec91085e2022-02-10 18:05:45 +0100211 (options, args) = parser.parse_args()
Philipp Maierc5b422e2019-08-30 11:41:02 +0200212
Harald Weltec91085e2022-02-10 18:05:45 +0100213 if options.type == 'list':
214 for kls in _cards_classes:
215 print(kls.name)
216 sys.exit(0)
Sylvain Munaut76504e02010-12-07 00:24:32 +0100217
Harald Weltec91085e2022-02-10 18:05:45 +0100218 if options.probe:
219 return options
Sylvain Munaut76504e02010-12-07 00:24:32 +0100220
Harald Weltec91085e2022-02-10 18:05:45 +0100221 if options.source == 'csv':
222 if (options.imsi is None) and (options.batch_mode is False) and (options.read_imsi is False) and (options.read_iccid is False):
223 parser.error(
224 "CSV mode needs either an IMSI, --read-imsi, --read-iccid or batch mode")
225 if options.read_csv is None:
226 parser.error("CSV mode requires a CSV input file")
227 elif options.source == 'cmdline':
228 if ((options.imsi is None) or (options.iccid is None)) and (options.num is None):
229 parser.error(
230 "If either IMSI or ICCID isn't specified, num is required")
231 else:
232 parser.error("Only `cmdline' and `csv' sources supported")
Philipp Maierac9dde62018-07-04 11:05:14 +0200233
Harald Weltec91085e2022-02-10 18:05:45 +0100234 if (options.read_csv is not None) and (options.source != 'csv'):
235 parser.error("You cannot specify a CSV input file in source != csv")
Harald Welte7f62cec2012-08-13 20:07:41 +0200236
Harald Weltec91085e2022-02-10 18:05:45 +0100237 if (options.batch_mode) and (options.num is None):
238 options.num = 0
Harald Welte7f62cec2012-08-13 20:07:41 +0200239
Harald Weltec91085e2022-02-10 18:05:45 +0100240 if (options.batch_mode):
241 if (options.imsi is not None) or (options.iccid is not None):
242 parser.error(
243 "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 +0100244
Harald Weltec91085e2022-02-10 18:05:45 +0100245 if args:
246 parser.error("Extraneous arguments")
Sylvain Munaut98d2b852010-12-23 20:27:25 +0100247
Harald Weltec91085e2022-02-10 18:05:45 +0100248 return options
Sylvain Munaut76504e02010-12-07 00:24:32 +0100249
250
251def _digits(secret, usage, len, num):
Harald Weltec91085e2022-02-10 18:05:45 +0100252 seed = secret + usage + '%d' % num
253 s = hashlib.sha1(seed.encode())
254 d = ''.join(['%02d' % x for x in s.digest()])
255 return d[0:len]
256
Sylvain Munaut76504e02010-12-07 00:24:32 +0100257
258def _mcc_mnc_digits(mcc, mnc):
Harald Weltec91085e2022-02-10 18:05:45 +0100259 return '%s%s' % (mcc, mnc)
260
Sylvain Munaut76504e02010-12-07 00:24:32 +0100261
262def _cc_digits(cc):
Harald Weltec91085e2022-02-10 18:05:45 +0100263 return ('%03d' if cc > 100 else '%02d') % cc
264
Sylvain Munaut76504e02010-12-07 00:24:32 +0100265
266def _isnum(s, l=-1):
Harald Weltec91085e2022-02-10 18:05:45 +0100267 return s.isdigit() and ((l == -1) or (len(s) == l))
268
Sylvain Munaut76504e02010-12-07 00:24:32 +0100269
Sylvain Munaut607ce2a2011-12-08 20:16:43 +0100270def _ishex(s, l=-1):
Harald Weltec91085e2022-02-10 18:05:45 +0100271 hc = '0123456789abcdef'
272 return all([x in hc for x in s.lower()]) and ((l == -1) or (len(s) == l))
Sylvain Munaut607ce2a2011-12-08 20:16:43 +0100273
Sylvain Munaut76504e02010-12-07 00:24:32 +0100274
Sylvain Munaut9f120e02010-12-23 20:28:24 +0100275def _dbi_binary_quote(s):
Harald Weltec91085e2022-02-10 18:05:45 +0100276 # Count usage of each char
277 cnt = {}
278 for c in s:
279 cnt[c] = cnt.get(c, 0) + 1
Sylvain Munaut9f120e02010-12-23 20:28:24 +0100280
Harald Weltec91085e2022-02-10 18:05:45 +0100281 # Find best offset
282 e = 0
283 m = len(s)
284 for i in range(1, 256):
285 if i == 39:
286 continue
287 sum_ = cnt.get(i, 0) + cnt.get((i+1) & 0xff, 0) + \
288 cnt.get((i+39) & 0xff, 0)
289 if sum_ < m:
290 m = sum_
291 e = i
292 if m == 0: # No overhead ? use this !
293 break
Sylvain Munaut1a914432011-12-08 20:08:26 +0100294
Harald Weltec91085e2022-02-10 18:05:45 +0100295 # Generate output
296 out = []
297 out.append(chr(e)) # Offset
298 for c in s:
299 x = (256 + ord(c) - e) % 256
300 if x in (0, 1, 39):
301 out.append('\x01')
302 out.append(chr(x+1))
303 else:
304 out.append(chr(x))
Sylvain Munaut9f120e02010-12-23 20:28:24 +0100305
Harald Weltec91085e2022-02-10 18:05:45 +0100306 return ''.join(out)
307
Sylvain Munaut9f120e02010-12-23 20:28:24 +0100308
Sylvain Munaut76504e02010-12-07 00:24:32 +0100309def gen_parameters(opts):
Harald Weltec91085e2022-02-10 18:05:45 +0100310 """Generates Name, ICCID, MCC, MNC, IMSI, SMSP, Ki, PIN-ADM from the
311 options given by the user"""
Sylvain Munaut76504e02010-12-07 00:24:32 +0100312
Harald Weltec91085e2022-02-10 18:05:45 +0100313 # MCC/MNC
314 mcc = opts.mcc
315 mnc = opts.mnc
Sylvain Munaut76504e02010-12-07 00:24:32 +0100316
Harald Weltec91085e2022-02-10 18:05:45 +0100317 if not mcc.isdigit() or not mnc.isdigit():
318 raise ValueError('mcc & mnc must only contain decimal digits')
319 if len(mcc) < 1 or len(mcc) > 3:
320 raise ValueError('mcc must be between 1 .. 3 digits')
321 if len(mnc) < 1 or len(mnc) > 3:
322 raise ValueError('mnc must be between 1 .. 3 digits')
Harald Welte7f1d3c42020-05-12 21:12:44 +0200323
Harald Weltec91085e2022-02-10 18:05:45 +0100324 # MCC always has 3 digits
325 mcc = lpad(mcc, 3, "0")
Philipp Maier32c04342022-12-16 16:39:24 +0100326
327 # The MNC must be at least 2 digits long. This is also the most common case.
328 # The user may specify an explicit length using the --mnclen option.
329 if opts.mnclen != "auto":
330 if len(mnc) > int(opts.mnclen):
331 raise ValueError('mcc is longer than specified in option --mnclen')
332 mnc = lpad(mnc, int(opts.mnclen), "0")
333 else:
334 mnc = lpad(mnc, 2, "0")
Sylvain Munaut76504e02010-12-07 00:24:32 +0100335
Harald Weltec91085e2022-02-10 18:05:45 +0100336 # Digitize country code (2 or 3 digits)
337 cc_digits = _cc_digits(opts.country)
Sylvain Munaut76504e02010-12-07 00:24:32 +0100338
Harald Weltec91085e2022-02-10 18:05:45 +0100339 # Digitize MCC/MNC (5 or 6 digits)
340 plmn_digits = _mcc_mnc_digits(mcc, mnc)
Sylvain Munaut76504e02010-12-07 00:24:32 +0100341
Harald Weltec91085e2022-02-10 18:05:45 +0100342 if opts.name is not None:
343 if len(opts.name) > 16:
344 raise ValueError('Service Provider Name must max 16 characters!')
Supreeth Herle840a9e22020-01-21 13:32:46 +0100345
Harald Weltec91085e2022-02-10 18:05:45 +0100346 if opts.msisdn is not None:
347 msisdn = opts.msisdn
348 if msisdn[0] == '+':
349 msisdn = msisdn[1:]
350 if not msisdn.isdigit():
351 raise ValueError('MSISDN must be digits only! '
352 'Start with \'+\' for international numbers.')
353 if len(msisdn) > 10 * 2:
354 # TODO: Support MSISDN of length > 20 (10 Bytes)
355 raise ValueError(
356 'MSISDNs longer than 20 digits are not (yet) supported.')
Supreeth Herle5a541012019-12-22 08:59:16 +0100357
Harald Weltec91085e2022-02-10 18:05:45 +0100358 # ICCID (19 digits, E.118), though some phase1 vendors use 20 :(
359 if opts.iccid is not None:
360 iccid = opts.iccid
Harald Welte284efda2023-07-11 11:08:24 +0200361 if not _isnum(iccid, 18) and not _isnum(iccid, 19) and not _isnum(iccid, 20):
362 raise ValueError('ICCID must be 18, 19 or 20 digits !')
Sylvain Munaut76504e02010-12-07 00:24:32 +0100363
Harald Weltec91085e2022-02-10 18:05:45 +0100364 else:
365 if opts.num is None:
366 raise ValueError('Neither ICCID nor card number specified !')
Sylvain Munaut76504e02010-12-07 00:24:32 +0100367
Harald Weltec91085e2022-02-10 18:05:45 +0100368 iccid = (
369 '89' + # Common prefix (telecom)
370 cc_digits + # Country Code on 2/3 digits
371 plmn_digits # MCC/MNC on 5/6 digits
372 )
Sylvain Munaut76504e02010-12-07 00:24:32 +0100373
Harald Weltec91085e2022-02-10 18:05:45 +0100374 ml = 18 - len(iccid)
Sylvain Munaut76504e02010-12-07 00:24:32 +0100375
Harald Weltec91085e2022-02-10 18:05:45 +0100376 if opts.secret is None:
377 # The raw number
378 iccid += ('%%0%dd' % ml) % opts.num
379 else:
380 # Randomized digits
381 iccid += _digits(opts.secret, 'ccid', ml, opts.num)
Sylvain Munaut76504e02010-12-07 00:24:32 +0100382
Harald Weltec91085e2022-02-10 18:05:45 +0100383 # Add checksum digit
384 iccid += ('%1d' % calculate_luhn(iccid))
Harald Welte2c0ff3a2011-12-07 12:34:13 +0100385
Harald Weltec91085e2022-02-10 18:05:45 +0100386 # IMSI (15 digits usually)
387 if opts.imsi is not None:
388 imsi = opts.imsi
389 if not _isnum(imsi):
390 raise ValueError('IMSI must be digits only !')
Sylvain Munaut76504e02010-12-07 00:24:32 +0100391
Harald Weltec91085e2022-02-10 18:05:45 +0100392 else:
393 if opts.num is None:
394 raise ValueError('Neither IMSI nor card number specified !')
Sylvain Munaut76504e02010-12-07 00:24:32 +0100395
Harald Weltec91085e2022-02-10 18:05:45 +0100396 ml = 15 - len(plmn_digits)
Sylvain Munaut76504e02010-12-07 00:24:32 +0100397
Harald Weltec91085e2022-02-10 18:05:45 +0100398 if opts.secret is None:
399 # The raw number
400 msin = ('%%0%dd' % ml) % opts.num
401 else:
402 # Randomized digits
403 msin = _digits(opts.secret, 'imsi', ml, opts.num)
Sylvain Munaut76504e02010-12-07 00:24:32 +0100404
Harald Weltec91085e2022-02-10 18:05:45 +0100405 imsi = (
406 plmn_digits + # MCC/MNC on 5/6 digits
407 msin # MSIN
408 )
Sylvain Munaut76504e02010-12-07 00:24:32 +0100409
Harald Weltec91085e2022-02-10 18:05:45 +0100410 # SMSP
411 if opts.smsp is not None:
412 smsp = opts.smsp
413 if not _ishex(smsp):
414 raise ValueError('SMSP must be hex digits only !')
415 if len(smsp) < 28*2:
416 raise ValueError('SMSP must be at least 28 bytes')
Sylvain Munaut76504e02010-12-07 00:24:32 +0100417
Harald Weltec91085e2022-02-10 18:05:45 +0100418 else:
419 ton = "81"
420 if opts.smsc is not None:
421 smsc = opts.smsc
422 if smsc[0] == '+':
423 ton = "91"
424 smsc = smsc[1:]
425 if not _isnum(smsc):
426 raise ValueError('SMSC must be digits only!\n \
Daniel Willmann4fa8f1c2018-10-02 18:10:21 +0200427 Start with \'+\' for international numbers')
Harald Weltec91085e2022-02-10 18:05:45 +0100428 else:
429 smsc = '00%d' % opts.country + '5555' # Hack ...
Sylvain Munaut607ce2a2011-12-08 20:16:43 +0100430
Harald Weltec91085e2022-02-10 18:05:45 +0100431 smsc = '%02d' % ((len(smsc) + 3)//2,) + ton + \
432 swap_nibbles(rpad(smsc, 20))
Sylvain Munaut607ce2a2011-12-08 20:16:43 +0100433
Harald Weltec91085e2022-02-10 18:05:45 +0100434 smsp = (
435 'e1' + # Parameters indicator
436 'ff' * 12 + # TP-Destination address
437 smsc + # TP-Service Centre Address
438 '00' + # TP-Protocol identifier
439 '00' + # TP-Data coding scheme
440 '00' # TP-Validity period
441 )
Sylvain Munaut76504e02010-12-07 00:24:32 +0100442
Harald Weltec91085e2022-02-10 18:05:45 +0100443 # ACC
444 if opts.acc is not None:
445 acc = opts.acc
446 if not _ishex(acc):
447 raise ValueError('ACC must be hex digits only !')
448 if len(acc) != 2*2:
449 raise ValueError('ACC must be exactly 2 bytes')
Alexander Chemeris21885242013-07-02 16:56:55 +0400450
Harald Weltec91085e2022-02-10 18:05:45 +0100451 else:
452 acc = None
Alexander Chemeris21885242013-07-02 16:56:55 +0400453
Harald Weltec91085e2022-02-10 18:05:45 +0100454 # Ki (random)
455 if opts.ki is not None:
456 ki = opts.ki
457 if not re.match('^[0-9a-fA-F]{32}$', ki):
458 raise ValueError('Ki needs to be 128 bits, in hex format')
459 else:
460 ki = ''.join(['%02x' % random.randrange(0, 256) for i in range(16)])
Sylvain Munaut76504e02010-12-07 00:24:32 +0100461
Harald Weltec91085e2022-02-10 18:05:45 +0100462 # OPC (random)
463 if opts.opc is not None:
464 opc = opts.opc
465 if not re.match('^[0-9a-fA-F]{32}$', opc):
466 raise ValueError('OPC needs to be 128 bits, in hex format')
Harald Welte93b38cd2012-03-22 14:31:36 +0100467
Harald Weltec91085e2022-02-10 18:05:45 +0100468 elif opts.op is not None:
469 opc = derive_milenage_opc(ki, opts.op)
470 else:
471 opc = ''.join(['%02x' % random.randrange(0, 256) for i in range(16)])
Harald Welte93b38cd2012-03-22 14:31:36 +0100472
Harald Weltec91085e2022-02-10 18:05:45 +0100473 pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
Harald Welte93b38cd2012-03-22 14:31:36 +0100474
Harald Weltec91085e2022-02-10 18:05:45 +0100475 # ePDG Selection Information
476 if opts.epdgSelection:
477 if len(opts.epdgSelection) < 5 or len(opts.epdgSelection) > 6:
478 raise ValueError('ePDG Selection Information is not valid')
479 epdg_mcc = opts.epdgSelection[:3]
480 epdg_mnc = opts.epdgSelection[3:]
481 if not epdg_mcc.isdigit() or not epdg_mnc.isdigit():
482 raise ValueError(
483 'PLMN for ePDG Selection must only contain decimal digits')
Supreeth Herlef964df42020-03-24 13:15:37 +0100484
Harald Weltec91085e2022-02-10 18:05:45 +0100485 # Return that
486 return {
487 'name': opts.name,
488 'iccid': iccid,
489 'mcc': mcc,
490 'mnc': mnc,
491 'imsi': imsi,
492 'smsp': smsp,
493 'ki': ki,
494 'opc': opc,
495 'acc': acc,
496 'pin_adm': pin_adm,
497 'msisdn': opts.msisdn,
498 'epdgid': opts.epdgid,
499 'epdgSelection': opts.epdgSelection,
500 'pcscf': opts.pcscf,
501 'ims_hdomain': opts.ims_hdomain,
502 'impi': opts.impi,
503 'impu': opts.impu,
504 'opmode': opts.opmode,
Matan Perelman777ee9e2023-05-14 08:58:50 +0300505 'fplmn': opts.fplmn,
Harald Weltec91085e2022-02-10 18:05:45 +0100506 }
Sylvain Munaut76504e02010-12-07 00:24:32 +0100507
508
509def print_parameters(params):
510
Harald Weltec91085e2022-02-10 18:05:45 +0100511 s = ["Generated card parameters :"]
512 if 'name' in params:
513 s.append(" > Name : %(name)s")
514 if 'smsp' in params:
515 s.append(" > SMSP : %(smsp)s")
516 s.append(" > ICCID : %(iccid)s")
517 s.append(" > MCC/MNC : %(mcc)s/%(mnc)s")
518 s.append(" > IMSI : %(imsi)s")
519 s.append(" > Ki : %(ki)s")
520 s.append(" > OPC : %(opc)s")
521 if 'acc' in params:
522 s.append(" > ACC : %(acc)s")
523 s.append(" > ADM1(hex): %(pin_adm)s")
524 if 'opmode' in params:
525 s.append(" > OPMODE : %(opmode)s")
526 print("\n".join(s) % params)
Sylvain Munaut76504e02010-12-07 00:24:32 +0100527
528
Harald Welte130524b2012-08-13 15:53:43 +0200529def write_params_csv(opts, params):
Harald Weltec91085e2022-02-10 18:05:45 +0100530 # csv
531 if opts.write_csv:
532 import csv
533 row = ['name', 'iccid', 'mcc', 'mnc', 'imsi', 'smsp', 'ki', 'opc']
534 f = open(opts.write_csv, 'a')
535 cw = csv.writer(f)
536 cw.writerow([params[x] for x in row])
537 f.close()
538
Sylvain Munaut143e99d2010-12-08 22:35:04 +0100539
Philipp Maier2688ddf2022-12-16 13:36:42 +0100540def find_row_in_csv_file(csv_file_name:str, num=None, iccid=None, imsi=None):
541 """
542 Pick a matching row in a CSV file by row number or ICCID or IMSI. When num
543 is not None, the search parameters iccid and imsi are ignored. When
544 searching for a specific ICCID or IMSI the caller must set num to None. It
545 is possible to search for an ICCID or an IMSI at the same time. The first
546 line that either contains a matching ICCID or IMSI is returned. Unused
547 search parameters must be set to None.
548 """
549
550 f = open(csv_file_name, 'r')
Harald Weltec91085e2022-02-10 18:05:45 +0100551 cr = csv.DictReader(f)
Philipp Maier120a0002019-09-12 13:11:45 +0200552
Philipp Maier2688ddf2022-12-16 13:36:42 +0100553 # Make sure the CSV file contains at least the fields we are searching for
554 if not 'iccid' in cr.fieldnames:
555 raise Exception("wrong CSV file format - no field \"iccid\" or missing header!")
556 if not 'imsi' in cr.fieldnames:
557 raise Exception("wrong CSV file format - no field \"imsi\" or missing header!")
558
559 # Enforce at least one search parameter
560 if not num and not iccid and not imsi:
561 raise Exception("no CSV file search parameters!")
562
Harald Weltec91085e2022-02-10 18:05:45 +0100563 # Lower-case fieldnames
564 cr.fieldnames = [field.lower() for field in cr.fieldnames]
Philipp Maier120a0002019-09-12 13:11:45 +0200565
Harald Weltec91085e2022-02-10 18:05:45 +0100566 i = 0
Harald Weltec91085e2022-02-10 18:05:45 +0100567 for row in cr:
Philipp Maier2688ddf2022-12-16 13:36:42 +0100568 # Pick a specific row by line number (num)
569 if num is not None and iccid is None and imsi is None:
Philipp Maier91c971b2023-10-09 10:48:46 +0200570 if num == i:
Harald Weltec91085e2022-02-10 18:05:45 +0100571 f.close()
572 return row
Philipp Maier2688ddf2022-12-16 13:36:42 +0100573
574 # Pick the first row that contains the specified ICCID
Harald Weltec91085e2022-02-10 18:05:45 +0100575 if row['iccid'] == iccid:
576 f.close()
577 return row
Daniel Willmann164b9632019-09-03 19:13:51 +0200578
Philipp Maier2688ddf2022-12-16 13:36:42 +0100579 # Pick the first row that contains the specified IMSI
Harald Weltec91085e2022-02-10 18:05:45 +0100580 if row['imsi'] == imsi:
581 f.close()
582 return row
Harald Welte7f62cec2012-08-13 20:07:41 +0200583
Philipp Maier2688ddf2022-12-16 13:36:42 +0100584 i += 1
585
Harald Weltec91085e2022-02-10 18:05:45 +0100586 f.close()
Philipp Maier2688ddf2022-12-16 13:36:42 +0100587 print("Could not read card parameters from CSV file, no matching entry found.")
Harald Weltec91085e2022-02-10 18:05:45 +0100588 return None
589
Harald Weltec26b8292012-08-15 15:25:51 +0200590
Daniel Willmann164b9632019-09-03 19:13:51 +0200591def read_params_csv(opts, imsi=None, iccid=None):
Philipp Maier4237ccf2022-12-20 11:21:47 +0100592 """
593 Read the card parameters from a CSV file. This function will generate the
594 same dictionary that gen_parameters would generate from parameters passed as
595 commandline arguments.
596 """
597
Philipp Maier2688ddf2022-12-16 13:36:42 +0100598 row = find_row_in_csv_file(opts.read_csv, opts.num, iccid=iccid, imsi=imsi)
Harald Weltec91085e2022-02-10 18:05:45 +0100599 if row is not None:
600 row['mcc'] = row.get('mcc', mcc_from_imsi(row.get('imsi')))
Philipp Maier32c04342022-12-16 16:39:24 +0100601
602 # We cannot determine the MNC length (2 or 3 digits?) from the IMSI
603 # alone. In cases where the user has specified an mnclen via the
604 # commandline options we can use that info, otherwise we guess that
605 # the length is 2, which is also the most common case.
606 if opts.mnclen != "auto":
Vadim Yanitskiy0d80fa92023-04-14 00:11:19 +0700607 if opts.mnclen == "2":
Philipp Maier32c04342022-12-16 16:39:24 +0100608 row['mnc'] = row.get('mnc', mnc_from_imsi(row.get('imsi'), False))
Vadim Yanitskiy0d80fa92023-04-14 00:11:19 +0700609 elif opts.mnclen == "3":
Philipp Maier32c04342022-12-16 16:39:24 +0100610 row['mnc'] = row.get('mnc', mnc_from_imsi(row.get('imsi'), True))
611 else:
612 raise ValueError("invalid parameter --mnclen, must be 2 or 3 or auto")
613 else:
614 row['mnc'] = row.get('mnc', mnc_from_imsi(row.get('imsi'), False))
615
616 # NOTE: We might concider to specify a new CSV field "mnclen" in our
617 # CSV files for a better automatization. However, this only makes sense
618 # when the tools and databases we export our files from will also add
619 # such a field.
Philipp Maier7592eee2019-09-12 13:03:23 +0200620
Harald Weltec91085e2022-02-10 18:05:45 +0100621 pin_adm = None
622 # We need to escape the pin_adm we get from the csv
623 if 'pin_adm' in row:
624 pin_adm = ''.join(['%02x' % (ord(x)) for x in row['pin_adm']])
625 # Stay compatible to the odoo csv format
626 elif 'adm1' in row:
627 pin_adm = ''.join(['%02x' % (ord(x)) for x in row['adm1']])
628 if pin_adm:
629 row['pin_adm'] = rpad(pin_adm, 16)
Philipp Maiere053da52019-09-05 13:08:36 +0200630
Harald Weltec91085e2022-02-10 18:05:45 +0100631 # If the CSV-File defines a pin_adm_hex field use this field to
632 # generate pin_adm from that.
633 pin_adm_hex = row.get('pin_adm_hex')
634 if pin_adm_hex:
635 if len(pin_adm_hex) == 16:
636 row['pin_adm'] = pin_adm_hex
637 # Ensure that it's hex-encoded
638 try:
639 try_encode = h2b(pin_adm)
640 except ValueError:
641 raise ValueError(
642 "pin_adm_hex needs to be hex encoded using this option")
643 else:
644 raise ValueError(
645 "pin_adm_hex needs to be exactly 16 digits (hex encoded)")
Philipp Maiere053da52019-09-05 13:08:36 +0200646
Harald Weltec91085e2022-02-10 18:05:45 +0100647 return row
Harald Welte7f62cec2012-08-13 20:07:41 +0200648
Harald Weltec26b8292012-08-15 15:25:51 +0200649
Harald Welte130524b2012-08-13 15:53:43 +0200650def write_params_hlr(opts, params):
Harald Weltec91085e2022-02-10 18:05:45 +0100651 # SQLite3 OpenBSC HLR
652 if opts.write_hlr:
653 import sqlite3
654 conn = sqlite3.connect(opts.write_hlr)
Sylvain Munaut143e99d2010-12-08 22:35:04 +0100655
Harald Weltec91085e2022-02-10 18:05:45 +0100656 c = conn.execute(
657 'INSERT INTO Subscriber ' +
658 '(imsi, name, extension, authorized, created, updated) ' +
659 'VALUES ' +
660 '(?,?,?,1,datetime(\'now\'),datetime(\'now\'));',
661 [
662 params['imsi'],
663 params['name'],
664 '9' + params['iccid'][-5:-1]
665 ],
666 )
667 sub_id = c.lastrowid
668 c.close()
Sylvain Munaut143e99d2010-12-08 22:35:04 +0100669
Harald Weltec91085e2022-02-10 18:05:45 +0100670 c = conn.execute(
671 'INSERT INTO AuthKeys ' +
672 '(subscriber_id, algorithm_id, a3a8_ki)' +
673 'VALUES ' +
674 '(?,?,?)',
675 [sub_id, 2, sqlite3.Binary(
676 _dbi_binary_quote(h2b(params['ki'])))],
677 )
Sylvain Munaut143e99d2010-12-08 22:35:04 +0100678
Harald Weltec91085e2022-02-10 18:05:45 +0100679 conn.commit()
680 conn.close()
681
Sylvain Munaut143e99d2010-12-08 22:35:04 +0100682
Philipp Maier5f0cb3c2022-12-16 17:03:01 +0100683def write_parameters_to_csv_and_hlr(opts, params):
Harald Weltec91085e2022-02-10 18:05:45 +0100684 write_params_csv(opts, params)
685 write_params_hlr(opts, params)
Harald Welte130524b2012-08-13 15:53:43 +0200686
Sylvain Munaut143e99d2010-12-08 22:35:04 +0100687
Harald Weltec91085e2022-02-10 18:05:45 +0100688BATCH_STATE = ['name', 'country', 'mcc', 'mnc', 'smsp', 'secret', 'num']
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100689BATCH_INCOMPATIBLE = ['iccid', 'imsi', 'ki']
Sylvain Munaut143e99d2010-12-08 22:35:04 +0100690
Harald Weltec91085e2022-02-10 18:05:45 +0100691
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100692def init_batch(opts):
Harald Weltec91085e2022-02-10 18:05:45 +0100693 # Need to do something ?
694 if not opts.batch_mode:
695 return
Sylvain Munaut76504e02010-12-07 00:24:32 +0100696
Harald Weltec91085e2022-02-10 18:05:45 +0100697 for k in BATCH_INCOMPATIBLE:
698 if getattr(opts, k):
699 print("Incompatible option with batch_state: %s" % (k,))
700 sys.exit(-1)
Sylvain Munaut76504e02010-12-07 00:24:32 +0100701
Harald Weltec91085e2022-02-10 18:05:45 +0100702 # Don't load state if there is none ...
703 if not opts.batch_state:
704 return
Sylvain Munaut76504e02010-12-07 00:24:32 +0100705
Harald Weltec91085e2022-02-10 18:05:45 +0100706 if not os.path.isfile(opts.batch_state):
707 print("No state file yet")
708 return
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100709
Harald Weltec91085e2022-02-10 18:05:45 +0100710 # Get stored data
711 fh = open(opts.batch_state)
712 d = json.loads(fh.read())
713 fh.close()
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100714
Harald Weltec91085e2022-02-10 18:05:45 +0100715 for k, v in d.iteritems():
716 setattr(opts, k, v)
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100717
718
719def save_batch(opts):
Harald Weltec91085e2022-02-10 18:05:45 +0100720 # Need to do something ?
721 if not opts.batch_mode or not opts.batch_state:
722 return
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100723
Harald Weltec91085e2022-02-10 18:05:45 +0100724 d = json.dumps(dict([(k, getattr(opts, k)) for k in BATCH_STATE]))
725 fh = open(opts.batch_state, 'w')
726 fh.write(d)
727 fh.close()
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100728
729
Philipp Maier91c971b2023-10-09 10:48:46 +0200730def process_card(scc, opts, first, ch):
Philipp Maierc5b422e2019-08-30 11:41:02 +0200731
Philipp Maiercbb8c022022-12-16 16:57:16 +0100732 # Connect transport
733 ch.get(first)
734
735 # Get card
736 card = card_detect(opts.type, scc)
737 if card is None:
738 print("No card detected!")
739 return -1
740
741 # Probe only
742 if opts.probe:
743 return 0
744
745 # Erase if requested (not in dry run mode!)
Harald Weltec91085e2022-02-10 18:05:45 +0100746 if opts.dry_run is False:
Harald Weltec91085e2022-02-10 18:05:45 +0100747 if opts.erase:
748 print("Formatting ...")
749 card.erase()
750 card.reset()
Philipp Maierc5b422e2019-08-30 11:41:02 +0200751
Harald Weltec91085e2022-02-10 18:05:45 +0100752 # Generate parameters
753 if opts.source == 'cmdline':
754 cp = gen_parameters(opts)
755 elif opts.source == 'csv':
756 imsi = None
757 iccid = None
758 if opts.read_iccid:
Harald Weltec91085e2022-02-10 18:05:45 +0100759 (res, _) = scc.read_binary(['3f00', '2fe2'], length=10)
760 iccid = dec_iccid(res)
761 elif opts.read_imsi:
Harald Weltec91085e2022-02-10 18:05:45 +0100762 (res, _) = scc.read_binary(EF['IMSI'])
763 imsi = swap_nibbles(res)[3:]
764 else:
765 imsi = opts.imsi
766 cp = read_params_csv(opts, imsi=imsi, iccid=iccid)
767 if cp is None:
Harald Weltec91085e2022-02-10 18:05:45 +0100768 return 2
769 print_parameters(cp)
Philipp Maierc5b422e2019-08-30 11:41:02 +0200770
Harald Weltec91085e2022-02-10 18:05:45 +0100771 if opts.dry_run is False:
772 # Program the card
773 print("Programming ...")
774 card.program(cp)
775 else:
776 print("Dry Run: NOT PROGRAMMING!")
Philipp Maierc5b422e2019-08-30 11:41:02 +0200777
Philipp Maier5f0cb3c2022-12-16 17:03:01 +0100778 # Write parameters to a specified CSV file or an HLR database (not the card)
779 write_parameters_to_csv_and_hlr(opts, cp)
Philipp Maierc5b422e2019-08-30 11:41:02 +0200780
Harald Weltec91085e2022-02-10 18:05:45 +0100781 # Batch mode state update and save
782 if opts.num is not None:
783 opts.num += 1
784 save_batch(opts)
Philipp Maierc5b422e2019-08-30 11:41:02 +0200785
Harald Weltec91085e2022-02-10 18:05:45 +0100786 ch.done()
787 return 0
Philipp Maierc5b422e2019-08-30 11:41:02 +0200788
789
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100790if __name__ == '__main__':
791
Harald Weltec91085e2022-02-10 18:05:45 +0100792 # Parse options
793 opts = parse_options()
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100794
Harald Weltec91085e2022-02-10 18:05:45 +0100795 # Init card reader driver
796 sl = init_reader(opts)
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100797
Harald Weltec91085e2022-02-10 18:05:45 +0100798 # Create command layer
799 scc = SimCardCommands(transport=sl)
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100800
Harald Weltec91085e2022-02-10 18:05:45 +0100801 # If we use a CSV file as data input, check if the CSV file exists.
802 if opts.source == 'csv':
803 print("Using CSV file as data input: " + str(opts.read_csv))
804 if not os.path.isfile(opts.read_csv):
805 print("CSV file not found!")
806 sys.exit(1)
Philipp Maier196b08c2019-09-12 11:49:44 +0200807
Harald Weltec91085e2022-02-10 18:05:45 +0100808 # Batch mode init
809 init_batch(opts)
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100810
Harald Weltec91085e2022-02-10 18:05:45 +0100811 if opts.card_handler_config:
812 ch = CardHandlerAuto(sl, opts.card_handler_config)
813 else:
814 ch = CardHandler(sl)
Philipp Maierc5b422e2019-08-30 11:41:02 +0200815
Harald Weltec91085e2022-02-10 18:05:45 +0100816 # Iterate
817 first = True
818 card = None
Sylvain Munaut1a914432011-12-08 20:08:26 +0100819
Harald Weltec91085e2022-02-10 18:05:45 +0100820 while 1:
821 try:
Philipp Maier91c971b2023-10-09 10:48:46 +0200822 rc = process_card(scc, opts, first, ch)
Harald Weltec91085e2022-02-10 18:05:45 +0100823 except (KeyboardInterrupt):
824 print("")
825 print("Terminated by user!")
826 sys.exit(0)
827 except (SystemExit):
828 raise
829 except:
830 print("")
831 print("Card programming failed with an exception:")
832 print("---------------------8<---------------------")
833 traceback.print_exc()
834 print("---------------------8<---------------------")
835 print("")
836 rc = -1
Harald Weltee9e5ecb2012-08-15 15:26:30 +0200837
Harald Weltec91085e2022-02-10 18:05:45 +0100838 # Something did not work as well as expected, however, lets
839 # make sure the card is pulled from the reader.
840 if rc != 0:
841 ch.error()
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100842
Harald Weltec91085e2022-02-10 18:05:45 +0100843 # If we are not in batch mode we are done in any case, so lets
844 # exit here.
845 if not opts.batch_mode:
846 sys.exit(rc)
Philipp Maierc5b422e2019-08-30 11:41:02 +0200847
Harald Weltec91085e2022-02-10 18:05:45 +0100848 first = False