blob: 7c644b79afed1dbc6a72df16959fbf314c34806a [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
Supreeth Herle4c306ab2020-03-18 11:38:00 +010039from pySim.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
Robert Falkenbergd0505bd2021-02-24 14:06:18 +010041from pySim.ts_51_011 import EF, EF_AD
Philipp Maierc5b422e2019-08-30 11:41:02 +020042from pySim.card_handler import *
Philipp Maier7592eee2019-09-12 13:03:23 +020043from pySim.utils import *
Sylvain Munaut76504e02010-12-07 00:24:32 +010044
Harald Weltec91085e2022-02-10 18:05:45 +010045
Sylvain Munaut76504e02010-12-07 00:24:32 +010046def parse_options():
47
Harald Weltec91085e2022-02-10 18:05:45 +010048 parser = OptionParser(usage="usage: %prog [options]")
Sylvain Munaut76504e02010-12-07 00:24:32 +010049
Harald Weltec91085e2022-02-10 18:05:45 +010050 parser.add_option("-d", "--device", dest="device", metavar="DEV",
51 help="Serial Device for SIM access [default: %default]",
52 default="/dev/ttyUSB0",
53 )
54 parser.add_option("-b", "--baud", dest="baudrate", type="int", metavar="BAUD",
55 help="Baudrate used for SIM access [default: %default]",
56 default=9600,
57 )
58 parser.add_option("-p", "--pcsc-device", dest="pcsc_dev", type='int', metavar="PCSC",
59 help="Which PC/SC reader number for SIM access",
60 default=None,
61 )
62 parser.add_option("--modem-device", dest="modem_dev", metavar="DEV",
63 help="Serial port of modem for Generic SIM Access (3GPP TS 27.007)",
64 default=None,
65 )
66 parser.add_option("--modem-baud", dest="modem_baud", type="int", metavar="BAUD",
67 help="Baudrate used for modem's port [default: %default]",
68 default=115200,
69 )
70 parser.add_option("--osmocon", dest="osmocon_sock", metavar="PATH",
71 help="Socket path for Calypso (e.g. Motorola C1XX) based reader (via OsmocomBB)",
72 default=None,
73 )
74 parser.add_option("-t", "--type", dest="type",
75 help="Card type (user -t list to view) [default: %default]",
76 default="auto",
77 )
78 parser.add_option("-T", "--probe", dest="probe",
79 help="Determine card type",
80 default=False, action="store_true"
81 )
82 parser.add_option("-a", "--pin-adm", dest="pin_adm",
83 help="ADM PIN used for provisioning (overwrites default)",
84 )
85 parser.add_option("-A", "--pin-adm-hex", dest="pin_adm_hex",
86 help="ADM PIN used for provisioning, as hex string (16 characters long",
87 )
88 parser.add_option("-e", "--erase", dest="erase", action='store_true',
89 help="Erase beforehand [default: %default]",
90 default=False,
91 )
Sylvain Munaut76504e02010-12-07 00:24:32 +010092
Harald Weltec91085e2022-02-10 18:05:45 +010093 parser.add_option("-S", "--source", dest="source",
94 help="Data Source[default: %default]",
95 default="cmdline",
96 )
Harald Welte7f62cec2012-08-13 20:07:41 +020097
Harald Weltec91085e2022-02-10 18:05:45 +010098 # if mode is "cmdline"
99 parser.add_option("-n", "--name", dest="name",
100 help="Operator name [default: %default]",
101 default="Magic",
102 )
103 parser.add_option("-c", "--country", dest="country", type="int", metavar="CC",
104 help="Country code [default: %default]",
105 default=1,
106 )
107 parser.add_option("-x", "--mcc", dest="mcc", type="string",
108 help="Mobile Country Code [default: %default]",
109 default="901",
110 )
111 parser.add_option("-y", "--mnc", dest="mnc", type="string",
112 help="Mobile Network Code [default: %default]",
113 default="55",
114 )
115 parser.add_option("--mnclen", dest="mnclen", type="choice",
116 help="Length of Mobile Network Code [default: %default]",
Philipp Maier32c04342022-12-16 16:39:24 +0100117 default="auto",
118 choices=["2", "3", "auto"],
Harald Weltec91085e2022-02-10 18:05:45 +0100119 )
120 parser.add_option("-m", "--smsc", dest="smsc",
121 help="SMSC number (Start with + for international no.) [default: '00 + country code + 5555']",
122 )
123 parser.add_option("-M", "--smsp", dest="smsp",
124 help="Raw SMSP content in hex [default: auto from SMSC]",
125 )
Sylvain Munaut76504e02010-12-07 00:24:32 +0100126
Harald Weltec91085e2022-02-10 18:05:45 +0100127 parser.add_option("-s", "--iccid", dest="iccid", metavar="ID",
128 help="Integrated Circuit Card ID",
129 )
130 parser.add_option("-i", "--imsi", dest="imsi",
131 help="International Mobile Subscriber Identity",
132 )
133 parser.add_option("--msisdn", dest="msisdn",
134 help="Mobile Subscriber Integrated Services Digital Number",
135 )
136 parser.add_option("-k", "--ki", dest="ki",
137 help="Ki (default is to randomize)",
138 )
139 parser.add_option("-o", "--opc", dest="opc",
140 help="OPC (default is to randomize)",
141 )
142 parser.add_option("--op", dest="op",
143 help="Set OP to derive OPC from OP and KI",
144 )
145 parser.add_option("--acc", dest="acc",
146 help="Set ACC bits (Access Control Code). not all card types are supported",
147 )
148 parser.add_option("--opmode", dest="opmode", type="choice",
149 help="Set UE Operation Mode in EF.AD (Administrative Data)",
150 default=None,
151 choices=['{:02X}'.format(int(m)) for m in EF_AD.OP_MODE],
152 )
Matan Perelman777ee9e2023-05-14 08:58:50 +0300153 parser.add_option("-f", "--fplmn", dest="fplmn", action="append",
154 help="Set Forbidden PLMN. Add multiple time for multiple FPLMNS",
155 )
Harald Weltec91085e2022-02-10 18:05:45 +0100156 parser.add_option("--epdgid", dest="epdgid",
157 help="Set Home Evolved Packet Data Gateway (ePDG) Identifier. (Only FQDN format supported)",
158 )
159 parser.add_option("--epdgSelection", dest="epdgSelection",
160 help="Set PLMN for ePDG Selection Information. (Only Operator Identifier FQDN format supported)",
161 )
162 parser.add_option("--pcscf", dest="pcscf",
163 help="Set Proxy Call Session Control Function (P-CSCF) Address. (Only FQDN format supported)",
164 )
165 parser.add_option("--ims-hdomain", dest="ims_hdomain",
166 help="Set IMS Home Network Domain Name in FQDN format",
167 )
168 parser.add_option("--impi", dest="impi",
169 help="Set IMS private user identity",
170 )
171 parser.add_option("--impu", dest="impu",
172 help="Set IMS public user identity",
173 )
174 parser.add_option("--read-imsi", dest="read_imsi", action="store_true",
175 help="Read the IMSI from the CARD", default=False
176 )
177 parser.add_option("--read-iccid", dest="read_iccid", action="store_true",
178 help="Read the ICCID from the CARD", default=False
179 )
180 parser.add_option("-z", "--secret", dest="secret", metavar="STR",
181 help="Secret used for ICCID/IMSI autogen",
182 )
183 parser.add_option("-j", "--num", dest="num", type=int,
184 help="Card # used for ICCID/IMSI autogen",
185 )
186 parser.add_option("--batch", dest="batch_mode",
187 help="Enable batch mode [default: %default]",
188 default=False, action='store_true',
189 )
190 parser.add_option("--batch-state", dest="batch_state", metavar="FILE",
191 help="Optional batch state file",
192 )
Sylvain Munaut76504e02010-12-07 00:24:32 +0100193
Harald Weltec91085e2022-02-10 18:05:45 +0100194 # if mode is "csv"
195 parser.add_option("--read-csv", dest="read_csv", metavar="FILE",
196 help="Read parameters from CSV file rather than command line")
Harald Welte7f62cec2012-08-13 20:07:41 +0200197
Harald Weltec91085e2022-02-10 18:05:45 +0100198 parser.add_option("--write-csv", dest="write_csv", metavar="FILE",
199 help="Append generated parameters in CSV file",
200 )
201 parser.add_option("--write-hlr", dest="write_hlr", metavar="FILE",
202 help="Append generated parameters to OpenBSC HLR sqlite3",
203 )
204 parser.add_option("--dry-run", dest="dry_run",
205 help="Perform a 'dry run', don't actually program the card",
206 default=False, action="store_true")
207 parser.add_option("--card_handler", dest="card_handler_config", metavar="FILE",
208 help="Use automatic card handling machine")
Harald Welte7f62cec2012-08-13 20:07:41 +0200209
Harald Weltec91085e2022-02-10 18:05:45 +0100210 (options, args) = parser.parse_args()
Philipp Maierc5b422e2019-08-30 11:41:02 +0200211
Harald Weltec91085e2022-02-10 18:05:45 +0100212 if options.type == 'list':
213 for kls in _cards_classes:
214 print(kls.name)
215 sys.exit(0)
Sylvain Munaut76504e02010-12-07 00:24:32 +0100216
Harald Weltec91085e2022-02-10 18:05:45 +0100217 if options.probe:
218 return options
Sylvain Munaut76504e02010-12-07 00:24:32 +0100219
Harald Weltec91085e2022-02-10 18:05:45 +0100220 if options.source == 'csv':
221 if (options.imsi is None) and (options.batch_mode is False) and (options.read_imsi is False) and (options.read_iccid is False):
222 parser.error(
223 "CSV mode needs either an IMSI, --read-imsi, --read-iccid or batch mode")
224 if options.read_csv is None:
225 parser.error("CSV mode requires a CSV input file")
226 elif options.source == 'cmdline':
227 if ((options.imsi is None) or (options.iccid is None)) and (options.num is None):
228 parser.error(
229 "If either IMSI or ICCID isn't specified, num is required")
230 else:
231 parser.error("Only `cmdline' and `csv' sources supported")
Philipp Maierac9dde62018-07-04 11:05:14 +0200232
Harald Weltec91085e2022-02-10 18:05:45 +0100233 if (options.read_csv is not None) and (options.source != 'csv'):
234 parser.error("You cannot specify a CSV input file in source != csv")
Harald Welte7f62cec2012-08-13 20:07:41 +0200235
Harald Weltec91085e2022-02-10 18:05:45 +0100236 if (options.batch_mode) and (options.num is None):
237 options.num = 0
Harald Welte7f62cec2012-08-13 20:07:41 +0200238
Harald Weltec91085e2022-02-10 18:05:45 +0100239 if (options.batch_mode):
240 if (options.imsi is not None) or (options.iccid is not None):
241 parser.error(
242 "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 +0100243
Harald Weltec91085e2022-02-10 18:05:45 +0100244 if args:
245 parser.error("Extraneous arguments")
Sylvain Munaut98d2b852010-12-23 20:27:25 +0100246
Harald Weltec91085e2022-02-10 18:05:45 +0100247 return options
Sylvain Munaut76504e02010-12-07 00:24:32 +0100248
249
250def _digits(secret, usage, len, num):
Harald Weltec91085e2022-02-10 18:05:45 +0100251 seed = secret + usage + '%d' % num
252 s = hashlib.sha1(seed.encode())
253 d = ''.join(['%02d' % x for x in s.digest()])
254 return d[0:len]
255
Sylvain Munaut76504e02010-12-07 00:24:32 +0100256
257def _mcc_mnc_digits(mcc, mnc):
Harald Weltec91085e2022-02-10 18:05:45 +0100258 return '%s%s' % (mcc, mnc)
259
Sylvain Munaut76504e02010-12-07 00:24:32 +0100260
261def _cc_digits(cc):
Harald Weltec91085e2022-02-10 18:05:45 +0100262 return ('%03d' if cc > 100 else '%02d') % cc
263
Sylvain Munaut76504e02010-12-07 00:24:32 +0100264
265def _isnum(s, l=-1):
Harald Weltec91085e2022-02-10 18:05:45 +0100266 return s.isdigit() and ((l == -1) or (len(s) == l))
267
Sylvain Munaut76504e02010-12-07 00:24:32 +0100268
Sylvain Munaut607ce2a2011-12-08 20:16:43 +0100269def _ishex(s, l=-1):
Harald Weltec91085e2022-02-10 18:05:45 +0100270 hc = '0123456789abcdef'
271 return all([x in hc for x in s.lower()]) and ((l == -1) or (len(s) == l))
Sylvain Munaut607ce2a2011-12-08 20:16:43 +0100272
Sylvain Munaut76504e02010-12-07 00:24:32 +0100273
Sylvain Munaut9f120e02010-12-23 20:28:24 +0100274def _dbi_binary_quote(s):
Harald Weltec91085e2022-02-10 18:05:45 +0100275 # Count usage of each char
276 cnt = {}
277 for c in s:
278 cnt[c] = cnt.get(c, 0) + 1
Sylvain Munaut9f120e02010-12-23 20:28:24 +0100279
Harald Weltec91085e2022-02-10 18:05:45 +0100280 # Find best offset
281 e = 0
282 m = len(s)
283 for i in range(1, 256):
284 if i == 39:
285 continue
286 sum_ = cnt.get(i, 0) + cnt.get((i+1) & 0xff, 0) + \
287 cnt.get((i+39) & 0xff, 0)
288 if sum_ < m:
289 m = sum_
290 e = i
291 if m == 0: # No overhead ? use this !
292 break
Sylvain Munaut1a914432011-12-08 20:08:26 +0100293
Harald Weltec91085e2022-02-10 18:05:45 +0100294 # Generate output
295 out = []
296 out.append(chr(e)) # Offset
297 for c in s:
298 x = (256 + ord(c) - e) % 256
299 if x in (0, 1, 39):
300 out.append('\x01')
301 out.append(chr(x+1))
302 else:
303 out.append(chr(x))
Sylvain Munaut9f120e02010-12-23 20:28:24 +0100304
Harald Weltec91085e2022-02-10 18:05:45 +0100305 return ''.join(out)
306
Sylvain Munaut9f120e02010-12-23 20:28:24 +0100307
Sylvain Munaut76504e02010-12-07 00:24:32 +0100308def gen_parameters(opts):
Harald Weltec91085e2022-02-10 18:05:45 +0100309 """Generates Name, ICCID, MCC, MNC, IMSI, SMSP, Ki, PIN-ADM from the
310 options given by the user"""
Sylvain Munaut76504e02010-12-07 00:24:32 +0100311
Harald Weltec91085e2022-02-10 18:05:45 +0100312 # MCC/MNC
313 mcc = opts.mcc
314 mnc = opts.mnc
Sylvain Munaut76504e02010-12-07 00:24:32 +0100315
Harald Weltec91085e2022-02-10 18:05:45 +0100316 if not mcc.isdigit() or not mnc.isdigit():
317 raise ValueError('mcc & mnc must only contain decimal digits')
318 if len(mcc) < 1 or len(mcc) > 3:
319 raise ValueError('mcc must be between 1 .. 3 digits')
320 if len(mnc) < 1 or len(mnc) > 3:
321 raise ValueError('mnc must be between 1 .. 3 digits')
Harald Welte7f1d3c42020-05-12 21:12:44 +0200322
Harald Weltec91085e2022-02-10 18:05:45 +0100323 # MCC always has 3 digits
324 mcc = lpad(mcc, 3, "0")
Philipp Maier32c04342022-12-16 16:39:24 +0100325
326 # The MNC must be at least 2 digits long. This is also the most common case.
327 # The user may specify an explicit length using the --mnclen option.
328 if opts.mnclen != "auto":
329 if len(mnc) > int(opts.mnclen):
330 raise ValueError('mcc is longer than specified in option --mnclen')
331 mnc = lpad(mnc, int(opts.mnclen), "0")
332 else:
333 mnc = lpad(mnc, 2, "0")
Sylvain Munaut76504e02010-12-07 00:24:32 +0100334
Harald Weltec91085e2022-02-10 18:05:45 +0100335 # Digitize country code (2 or 3 digits)
336 cc_digits = _cc_digits(opts.country)
Sylvain Munaut76504e02010-12-07 00:24:32 +0100337
Harald Weltec91085e2022-02-10 18:05:45 +0100338 # Digitize MCC/MNC (5 or 6 digits)
339 plmn_digits = _mcc_mnc_digits(mcc, mnc)
Sylvain Munaut76504e02010-12-07 00:24:32 +0100340
Harald Weltec91085e2022-02-10 18:05:45 +0100341 if opts.name is not None:
342 if len(opts.name) > 16:
343 raise ValueError('Service Provider Name must max 16 characters!')
Supreeth Herle840a9e22020-01-21 13:32:46 +0100344
Harald Weltec91085e2022-02-10 18:05:45 +0100345 if opts.msisdn is not None:
346 msisdn = opts.msisdn
347 if msisdn[0] == '+':
348 msisdn = msisdn[1:]
349 if not msisdn.isdigit():
350 raise ValueError('MSISDN must be digits only! '
351 'Start with \'+\' for international numbers.')
352 if len(msisdn) > 10 * 2:
353 # TODO: Support MSISDN of length > 20 (10 Bytes)
354 raise ValueError(
355 'MSISDNs longer than 20 digits are not (yet) supported.')
Supreeth Herle5a541012019-12-22 08:59:16 +0100356
Harald Weltec91085e2022-02-10 18:05:45 +0100357 # ICCID (19 digits, E.118), though some phase1 vendors use 20 :(
358 if opts.iccid is not None:
359 iccid = opts.iccid
360 if not _isnum(iccid, 19) and not _isnum(iccid, 20):
361 raise ValueError('ICCID must be 19 or 20 digits !')
Sylvain Munaut76504e02010-12-07 00:24:32 +0100362
Harald Weltec91085e2022-02-10 18:05:45 +0100363 else:
364 if opts.num is None:
365 raise ValueError('Neither ICCID nor card number specified !')
Sylvain Munaut76504e02010-12-07 00:24:32 +0100366
Harald Weltec91085e2022-02-10 18:05:45 +0100367 iccid = (
368 '89' + # Common prefix (telecom)
369 cc_digits + # Country Code on 2/3 digits
370 plmn_digits # MCC/MNC on 5/6 digits
371 )
Sylvain Munaut76504e02010-12-07 00:24:32 +0100372
Harald Weltec91085e2022-02-10 18:05:45 +0100373 ml = 18 - len(iccid)
Sylvain Munaut76504e02010-12-07 00:24:32 +0100374
Harald Weltec91085e2022-02-10 18:05:45 +0100375 if opts.secret is None:
376 # The raw number
377 iccid += ('%%0%dd' % ml) % opts.num
378 else:
379 # Randomized digits
380 iccid += _digits(opts.secret, 'ccid', ml, opts.num)
Sylvain Munaut76504e02010-12-07 00:24:32 +0100381
Harald Weltec91085e2022-02-10 18:05:45 +0100382 # Add checksum digit
383 iccid += ('%1d' % calculate_luhn(iccid))
Harald Welte2c0ff3a2011-12-07 12:34:13 +0100384
Harald Weltec91085e2022-02-10 18:05:45 +0100385 # IMSI (15 digits usually)
386 if opts.imsi is not None:
387 imsi = opts.imsi
388 if not _isnum(imsi):
389 raise ValueError('IMSI must be digits only !')
Sylvain Munaut76504e02010-12-07 00:24:32 +0100390
Harald Weltec91085e2022-02-10 18:05:45 +0100391 else:
392 if opts.num is None:
393 raise ValueError('Neither IMSI nor card number specified !')
Sylvain Munaut76504e02010-12-07 00:24:32 +0100394
Harald Weltec91085e2022-02-10 18:05:45 +0100395 ml = 15 - len(plmn_digits)
Sylvain Munaut76504e02010-12-07 00:24:32 +0100396
Harald Weltec91085e2022-02-10 18:05:45 +0100397 if opts.secret is None:
398 # The raw number
399 msin = ('%%0%dd' % ml) % opts.num
400 else:
401 # Randomized digits
402 msin = _digits(opts.secret, 'imsi', ml, opts.num)
Sylvain Munaut76504e02010-12-07 00:24:32 +0100403
Harald Weltec91085e2022-02-10 18:05:45 +0100404 imsi = (
405 plmn_digits + # MCC/MNC on 5/6 digits
406 msin # MSIN
407 )
Sylvain Munaut76504e02010-12-07 00:24:32 +0100408
Harald Weltec91085e2022-02-10 18:05:45 +0100409 # SMSP
410 if opts.smsp is not None:
411 smsp = opts.smsp
412 if not _ishex(smsp):
413 raise ValueError('SMSP must be hex digits only !')
414 if len(smsp) < 28*2:
415 raise ValueError('SMSP must be at least 28 bytes')
Sylvain Munaut76504e02010-12-07 00:24:32 +0100416
Harald Weltec91085e2022-02-10 18:05:45 +0100417 else:
418 ton = "81"
419 if opts.smsc is not None:
420 smsc = opts.smsc
421 if smsc[0] == '+':
422 ton = "91"
423 smsc = smsc[1:]
424 if not _isnum(smsc):
425 raise ValueError('SMSC must be digits only!\n \
Daniel Willmann4fa8f1c2018-10-02 18:10:21 +0200426 Start with \'+\' for international numbers')
Harald Weltec91085e2022-02-10 18:05:45 +0100427 else:
428 smsc = '00%d' % opts.country + '5555' # Hack ...
Sylvain Munaut607ce2a2011-12-08 20:16:43 +0100429
Harald Weltec91085e2022-02-10 18:05:45 +0100430 smsc = '%02d' % ((len(smsc) + 3)//2,) + ton + \
431 swap_nibbles(rpad(smsc, 20))
Sylvain Munaut607ce2a2011-12-08 20:16:43 +0100432
Harald Weltec91085e2022-02-10 18:05:45 +0100433 smsp = (
434 'e1' + # Parameters indicator
435 'ff' * 12 + # TP-Destination address
436 smsc + # TP-Service Centre Address
437 '00' + # TP-Protocol identifier
438 '00' + # TP-Data coding scheme
439 '00' # TP-Validity period
440 )
Sylvain Munaut76504e02010-12-07 00:24:32 +0100441
Harald Weltec91085e2022-02-10 18:05:45 +0100442 # ACC
443 if opts.acc is not None:
444 acc = opts.acc
445 if not _ishex(acc):
446 raise ValueError('ACC must be hex digits only !')
447 if len(acc) != 2*2:
448 raise ValueError('ACC must be exactly 2 bytes')
Alexander Chemeris21885242013-07-02 16:56:55 +0400449
Harald Weltec91085e2022-02-10 18:05:45 +0100450 else:
451 acc = None
Alexander Chemeris21885242013-07-02 16:56:55 +0400452
Harald Weltec91085e2022-02-10 18:05:45 +0100453 # Ki (random)
454 if opts.ki is not None:
455 ki = opts.ki
456 if not re.match('^[0-9a-fA-F]{32}$', ki):
457 raise ValueError('Ki needs to be 128 bits, in hex format')
458 else:
459 ki = ''.join(['%02x' % random.randrange(0, 256) for i in range(16)])
Sylvain Munaut76504e02010-12-07 00:24:32 +0100460
Harald Weltec91085e2022-02-10 18:05:45 +0100461 # OPC (random)
462 if opts.opc is not None:
463 opc = opts.opc
464 if not re.match('^[0-9a-fA-F]{32}$', opc):
465 raise ValueError('OPC needs to be 128 bits, in hex format')
Harald Welte93b38cd2012-03-22 14:31:36 +0100466
Harald Weltec91085e2022-02-10 18:05:45 +0100467 elif opts.op is not None:
468 opc = derive_milenage_opc(ki, opts.op)
469 else:
470 opc = ''.join(['%02x' % random.randrange(0, 256) for i in range(16)])
Harald Welte93b38cd2012-03-22 14:31:36 +0100471
Harald Weltec91085e2022-02-10 18:05:45 +0100472 pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
Harald Welte93b38cd2012-03-22 14:31:36 +0100473
Harald Weltec91085e2022-02-10 18:05:45 +0100474 # ePDG Selection Information
475 if opts.epdgSelection:
476 if len(opts.epdgSelection) < 5 or len(opts.epdgSelection) > 6:
477 raise ValueError('ePDG Selection Information is not valid')
478 epdg_mcc = opts.epdgSelection[:3]
479 epdg_mnc = opts.epdgSelection[3:]
480 if not epdg_mcc.isdigit() or not epdg_mnc.isdigit():
481 raise ValueError(
482 'PLMN for ePDG Selection must only contain decimal digits')
Supreeth Herlef964df42020-03-24 13:15:37 +0100483
Harald Weltec91085e2022-02-10 18:05:45 +0100484 # Return that
485 return {
486 'name': opts.name,
487 'iccid': iccid,
488 'mcc': mcc,
489 'mnc': mnc,
490 'imsi': imsi,
491 'smsp': smsp,
492 'ki': ki,
493 'opc': opc,
494 'acc': acc,
495 'pin_adm': pin_adm,
496 'msisdn': opts.msisdn,
497 'epdgid': opts.epdgid,
498 'epdgSelection': opts.epdgSelection,
499 'pcscf': opts.pcscf,
500 'ims_hdomain': opts.ims_hdomain,
501 'impi': opts.impi,
502 'impu': opts.impu,
503 'opmode': opts.opmode,
Matan Perelman777ee9e2023-05-14 08:58:50 +0300504 'fplmn': opts.fplmn,
Harald Weltec91085e2022-02-10 18:05:45 +0100505 }
Sylvain Munaut76504e02010-12-07 00:24:32 +0100506
507
508def print_parameters(params):
509
Harald Weltec91085e2022-02-10 18:05:45 +0100510 s = ["Generated card parameters :"]
511 if 'name' in params:
512 s.append(" > Name : %(name)s")
513 if 'smsp' in params:
514 s.append(" > SMSP : %(smsp)s")
515 s.append(" > ICCID : %(iccid)s")
516 s.append(" > MCC/MNC : %(mcc)s/%(mnc)s")
517 s.append(" > IMSI : %(imsi)s")
518 s.append(" > Ki : %(ki)s")
519 s.append(" > OPC : %(opc)s")
520 if 'acc' in params:
521 s.append(" > ACC : %(acc)s")
522 s.append(" > ADM1(hex): %(pin_adm)s")
523 if 'opmode' in params:
524 s.append(" > OPMODE : %(opmode)s")
525 print("\n".join(s) % params)
Sylvain Munaut76504e02010-12-07 00:24:32 +0100526
527
Harald Welte130524b2012-08-13 15:53:43 +0200528def write_params_csv(opts, params):
Harald Weltec91085e2022-02-10 18:05:45 +0100529 # csv
530 if opts.write_csv:
531 import csv
532 row = ['name', 'iccid', 'mcc', 'mnc', 'imsi', 'smsp', 'ki', 'opc']
533 f = open(opts.write_csv, 'a')
534 cw = csv.writer(f)
535 cw.writerow([params[x] for x in row])
536 f.close()
537
Sylvain Munaut143e99d2010-12-08 22:35:04 +0100538
Philipp Maier2688ddf2022-12-16 13:36:42 +0100539def find_row_in_csv_file(csv_file_name:str, num=None, iccid=None, imsi=None):
540 """
541 Pick a matching row in a CSV file by row number or ICCID or IMSI. When num
542 is not None, the search parameters iccid and imsi are ignored. When
543 searching for a specific ICCID or IMSI the caller must set num to None. It
544 is possible to search for an ICCID or an IMSI at the same time. The first
545 line that either contains a matching ICCID or IMSI is returned. Unused
546 search parameters must be set to None.
547 """
548
549 f = open(csv_file_name, 'r')
Harald Weltec91085e2022-02-10 18:05:45 +0100550 cr = csv.DictReader(f)
Philipp Maier120a0002019-09-12 13:11:45 +0200551
Philipp Maier2688ddf2022-12-16 13:36:42 +0100552 # Make sure the CSV file contains at least the fields we are searching for
553 if not 'iccid' in cr.fieldnames:
554 raise Exception("wrong CSV file format - no field \"iccid\" or missing header!")
555 if not 'imsi' in cr.fieldnames:
556 raise Exception("wrong CSV file format - no field \"imsi\" or missing header!")
557
558 # Enforce at least one search parameter
559 if not num and not iccid and not imsi:
560 raise Exception("no CSV file search parameters!")
561
Harald Weltec91085e2022-02-10 18:05:45 +0100562 # Lower-case fieldnames
563 cr.fieldnames = [field.lower() for field in cr.fieldnames]
Philipp Maier120a0002019-09-12 13:11:45 +0200564
Harald Weltec91085e2022-02-10 18:05:45 +0100565 i = 0
Harald Weltec91085e2022-02-10 18:05:45 +0100566 for row in cr:
Philipp Maier2688ddf2022-12-16 13:36:42 +0100567 # Pick a specific row by line number (num)
568 if num is not None and iccid is None and imsi is None:
Harald Weltec91085e2022-02-10 18:05:45 +0100569 if opts.num == i:
570 f.close()
571 return row
Philipp Maier2688ddf2022-12-16 13:36:42 +0100572
573 # Pick the first row that contains the specified ICCID
Harald Weltec91085e2022-02-10 18:05:45 +0100574 if row['iccid'] == iccid:
575 f.close()
576 return row
Daniel Willmann164b9632019-09-03 19:13:51 +0200577
Philipp Maier2688ddf2022-12-16 13:36:42 +0100578 # Pick the first row that contains the specified IMSI
Harald Weltec91085e2022-02-10 18:05:45 +0100579 if row['imsi'] == imsi:
580 f.close()
581 return row
Harald Welte7f62cec2012-08-13 20:07:41 +0200582
Philipp Maier2688ddf2022-12-16 13:36:42 +0100583 i += 1
584
Harald Weltec91085e2022-02-10 18:05:45 +0100585 f.close()
Philipp Maier2688ddf2022-12-16 13:36:42 +0100586 print("Could not read card parameters from CSV file, no matching entry found.")
Harald Weltec91085e2022-02-10 18:05:45 +0100587 return None
588
Harald Weltec26b8292012-08-15 15:25:51 +0200589
Daniel Willmann164b9632019-09-03 19:13:51 +0200590def read_params_csv(opts, imsi=None, iccid=None):
Philipp Maier4237ccf2022-12-20 11:21:47 +0100591 """
592 Read the card parameters from a CSV file. This function will generate the
593 same dictionary that gen_parameters would generate from parameters passed as
594 commandline arguments.
595 """
596
Philipp Maier2688ddf2022-12-16 13:36:42 +0100597 row = find_row_in_csv_file(opts.read_csv, opts.num, iccid=iccid, imsi=imsi)
Harald Weltec91085e2022-02-10 18:05:45 +0100598 if row is not None:
599 row['mcc'] = row.get('mcc', mcc_from_imsi(row.get('imsi')))
Philipp Maier32c04342022-12-16 16:39:24 +0100600
601 # We cannot determine the MNC length (2 or 3 digits?) from the IMSI
602 # alone. In cases where the user has specified an mnclen via the
603 # commandline options we can use that info, otherwise we guess that
604 # the length is 2, which is also the most common case.
605 if opts.mnclen != "auto":
Vadim Yanitskiy0d80fa92023-04-14 00:11:19 +0700606 if opts.mnclen == "2":
Philipp Maier32c04342022-12-16 16:39:24 +0100607 row['mnc'] = row.get('mnc', mnc_from_imsi(row.get('imsi'), False))
Vadim Yanitskiy0d80fa92023-04-14 00:11:19 +0700608 elif opts.mnclen == "3":
Philipp Maier32c04342022-12-16 16:39:24 +0100609 row['mnc'] = row.get('mnc', mnc_from_imsi(row.get('imsi'), True))
610 else:
611 raise ValueError("invalid parameter --mnclen, must be 2 or 3 or auto")
612 else:
613 row['mnc'] = row.get('mnc', mnc_from_imsi(row.get('imsi'), False))
614
615 # NOTE: We might concider to specify a new CSV field "mnclen" in our
616 # CSV files for a better automatization. However, this only makes sense
617 # when the tools and databases we export our files from will also add
618 # such a field.
Philipp Maier7592eee2019-09-12 13:03:23 +0200619
Harald Weltec91085e2022-02-10 18:05:45 +0100620 pin_adm = None
621 # We need to escape the pin_adm we get from the csv
622 if 'pin_adm' in row:
623 pin_adm = ''.join(['%02x' % (ord(x)) for x in row['pin_adm']])
624 # Stay compatible to the odoo csv format
625 elif 'adm1' in row:
626 pin_adm = ''.join(['%02x' % (ord(x)) for x in row['adm1']])
627 if pin_adm:
628 row['pin_adm'] = rpad(pin_adm, 16)
Philipp Maiere053da52019-09-05 13:08:36 +0200629
Harald Weltec91085e2022-02-10 18:05:45 +0100630 # If the CSV-File defines a pin_adm_hex field use this field to
631 # generate pin_adm from that.
632 pin_adm_hex = row.get('pin_adm_hex')
633 if pin_adm_hex:
634 if len(pin_adm_hex) == 16:
635 row['pin_adm'] = pin_adm_hex
636 # Ensure that it's hex-encoded
637 try:
638 try_encode = h2b(pin_adm)
639 except ValueError:
640 raise ValueError(
641 "pin_adm_hex needs to be hex encoded using this option")
642 else:
643 raise ValueError(
644 "pin_adm_hex needs to be exactly 16 digits (hex encoded)")
Philipp Maiere053da52019-09-05 13:08:36 +0200645
Harald Weltec91085e2022-02-10 18:05:45 +0100646 return row
Harald Welte7f62cec2012-08-13 20:07:41 +0200647
Harald Weltec26b8292012-08-15 15:25:51 +0200648
Harald Welte130524b2012-08-13 15:53:43 +0200649def write_params_hlr(opts, params):
Harald Weltec91085e2022-02-10 18:05:45 +0100650 # SQLite3 OpenBSC HLR
651 if opts.write_hlr:
652 import sqlite3
653 conn = sqlite3.connect(opts.write_hlr)
Sylvain Munaut143e99d2010-12-08 22:35:04 +0100654
Harald Weltec91085e2022-02-10 18:05:45 +0100655 c = conn.execute(
656 'INSERT INTO Subscriber ' +
657 '(imsi, name, extension, authorized, created, updated) ' +
658 'VALUES ' +
659 '(?,?,?,1,datetime(\'now\'),datetime(\'now\'));',
660 [
661 params['imsi'],
662 params['name'],
663 '9' + params['iccid'][-5:-1]
664 ],
665 )
666 sub_id = c.lastrowid
667 c.close()
Sylvain Munaut143e99d2010-12-08 22:35:04 +0100668
Harald Weltec91085e2022-02-10 18:05:45 +0100669 c = conn.execute(
670 'INSERT INTO AuthKeys ' +
671 '(subscriber_id, algorithm_id, a3a8_ki)' +
672 'VALUES ' +
673 '(?,?,?)',
674 [sub_id, 2, sqlite3.Binary(
675 _dbi_binary_quote(h2b(params['ki'])))],
676 )
Sylvain Munaut143e99d2010-12-08 22:35:04 +0100677
Harald Weltec91085e2022-02-10 18:05:45 +0100678 conn.commit()
679 conn.close()
680
Sylvain Munaut143e99d2010-12-08 22:35:04 +0100681
Philipp Maier5f0cb3c2022-12-16 17:03:01 +0100682def write_parameters_to_csv_and_hlr(opts, params):
Harald Weltec91085e2022-02-10 18:05:45 +0100683 write_params_csv(opts, params)
684 write_params_hlr(opts, params)
Harald Welte130524b2012-08-13 15:53:43 +0200685
Sylvain Munaut143e99d2010-12-08 22:35:04 +0100686
Harald Weltec91085e2022-02-10 18:05:45 +0100687BATCH_STATE = ['name', 'country', 'mcc', 'mnc', 'smsp', 'secret', 'num']
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100688BATCH_INCOMPATIBLE = ['iccid', 'imsi', 'ki']
Sylvain Munaut143e99d2010-12-08 22:35:04 +0100689
Harald Weltec91085e2022-02-10 18:05:45 +0100690
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100691def init_batch(opts):
Harald Weltec91085e2022-02-10 18:05:45 +0100692 # Need to do something ?
693 if not opts.batch_mode:
694 return
Sylvain Munaut76504e02010-12-07 00:24:32 +0100695
Harald Weltec91085e2022-02-10 18:05:45 +0100696 for k in BATCH_INCOMPATIBLE:
697 if getattr(opts, k):
698 print("Incompatible option with batch_state: %s" % (k,))
699 sys.exit(-1)
Sylvain Munaut76504e02010-12-07 00:24:32 +0100700
Harald Weltec91085e2022-02-10 18:05:45 +0100701 # Don't load state if there is none ...
702 if not opts.batch_state:
703 return
Sylvain Munaut76504e02010-12-07 00:24:32 +0100704
Harald Weltec91085e2022-02-10 18:05:45 +0100705 if not os.path.isfile(opts.batch_state):
706 print("No state file yet")
707 return
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100708
Harald Weltec91085e2022-02-10 18:05:45 +0100709 # Get stored data
710 fh = open(opts.batch_state)
711 d = json.loads(fh.read())
712 fh.close()
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100713
Harald Weltec91085e2022-02-10 18:05:45 +0100714 for k, v in d.iteritems():
715 setattr(opts, k, v)
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100716
717
718def save_batch(opts):
Harald Weltec91085e2022-02-10 18:05:45 +0100719 # Need to do something ?
720 if not opts.batch_mode or not opts.batch_state:
721 return
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100722
Harald Weltec91085e2022-02-10 18:05:45 +0100723 d = json.dumps(dict([(k, getattr(opts, k)) for k in BATCH_STATE]))
724 fh = open(opts.batch_state, 'w')
725 fh.write(d)
726 fh.close()
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100727
728
Philipp Maier82511e52021-09-17 13:38:36 +0200729def process_card(opts, first, ch):
Philipp Maierc5b422e2019-08-30 11:41:02 +0200730
Philipp Maiercbb8c022022-12-16 16:57:16 +0100731 # Connect transport
732 ch.get(first)
733
734 # Get card
735 card = card_detect(opts.type, scc)
736 if card is None:
737 print("No card detected!")
738 return -1
739
740 # Probe only
741 if opts.probe:
742 return 0
743
744 # Erase if requested (not in dry run mode!)
Harald Weltec91085e2022-02-10 18:05:45 +0100745 if opts.dry_run is False:
Harald Weltec91085e2022-02-10 18:05:45 +0100746 if opts.erase:
747 print("Formatting ...")
748 card.erase()
749 card.reset()
Philipp Maierc5b422e2019-08-30 11:41:02 +0200750
Harald Weltec91085e2022-02-10 18:05:45 +0100751 # Generate parameters
752 if opts.source == 'cmdline':
753 cp = gen_parameters(opts)
754 elif opts.source == 'csv':
755 imsi = None
756 iccid = None
757 if opts.read_iccid:
Harald Weltec91085e2022-02-10 18:05:45 +0100758 (res, _) = scc.read_binary(['3f00', '2fe2'], length=10)
759 iccid = dec_iccid(res)
760 elif opts.read_imsi:
Harald Weltec91085e2022-02-10 18:05:45 +0100761 (res, _) = scc.read_binary(EF['IMSI'])
762 imsi = swap_nibbles(res)[3:]
763 else:
764 imsi = opts.imsi
765 cp = read_params_csv(opts, imsi=imsi, iccid=iccid)
766 if cp is None:
Harald Weltec91085e2022-02-10 18:05:45 +0100767 return 2
768 print_parameters(cp)
Philipp Maierc5b422e2019-08-30 11:41:02 +0200769
Harald Weltec91085e2022-02-10 18:05:45 +0100770 if opts.dry_run is False:
771 # Program the card
772 print("Programming ...")
773 card.program(cp)
774 else:
775 print("Dry Run: NOT PROGRAMMING!")
Philipp Maierc5b422e2019-08-30 11:41:02 +0200776
Philipp Maier5f0cb3c2022-12-16 17:03:01 +0100777 # Write parameters to a specified CSV file or an HLR database (not the card)
778 write_parameters_to_csv_and_hlr(opts, cp)
Philipp Maierc5b422e2019-08-30 11:41:02 +0200779
Harald Weltec91085e2022-02-10 18:05:45 +0100780 # Batch mode state update and save
781 if opts.num is not None:
782 opts.num += 1
783 save_batch(opts)
Philipp Maierc5b422e2019-08-30 11:41:02 +0200784
Harald Weltec91085e2022-02-10 18:05:45 +0100785 ch.done()
786 return 0
Philipp Maierc5b422e2019-08-30 11:41:02 +0200787
788
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100789if __name__ == '__main__':
790
Harald Weltec91085e2022-02-10 18:05:45 +0100791 # Parse options
792 opts = parse_options()
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100793
Harald Weltec91085e2022-02-10 18:05:45 +0100794 # Init card reader driver
795 sl = init_reader(opts)
796 if sl is None:
797 exit(1)
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100798
Harald Weltec91085e2022-02-10 18:05:45 +0100799 # Create command layer
800 scc = SimCardCommands(transport=sl)
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100801
Harald Weltec91085e2022-02-10 18:05:45 +0100802 # If we use a CSV file as data input, check if the CSV file exists.
803 if opts.source == 'csv':
804 print("Using CSV file as data input: " + str(opts.read_csv))
805 if not os.path.isfile(opts.read_csv):
806 print("CSV file not found!")
807 sys.exit(1)
Philipp Maier196b08c2019-09-12 11:49:44 +0200808
Harald Weltec91085e2022-02-10 18:05:45 +0100809 # Batch mode init
810 init_batch(opts)
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100811
Harald Weltec91085e2022-02-10 18:05:45 +0100812 if opts.card_handler_config:
813 ch = CardHandlerAuto(sl, opts.card_handler_config)
814 else:
815 ch = CardHandler(sl)
Philipp Maierc5b422e2019-08-30 11:41:02 +0200816
Harald Weltec91085e2022-02-10 18:05:45 +0100817 # Iterate
818 first = True
819 card = None
Sylvain Munaut1a914432011-12-08 20:08:26 +0100820
Harald Weltec91085e2022-02-10 18:05:45 +0100821 while 1:
822 try:
823 rc = process_card(opts, first, ch)
824 except (KeyboardInterrupt):
825 print("")
826 print("Terminated by user!")
827 sys.exit(0)
828 except (SystemExit):
829 raise
830 except:
831 print("")
832 print("Card programming failed with an exception:")
833 print("---------------------8<---------------------")
834 traceback.print_exc()
835 print("---------------------8<---------------------")
836 print("")
837 rc = -1
Harald Weltee9e5ecb2012-08-15 15:26:30 +0200838
Harald Weltec91085e2022-02-10 18:05:45 +0100839 # Something did not work as well as expected, however, lets
840 # make sure the card is pulled from the reader.
841 if rc != 0:
842 ch.error()
Sylvain Munaut8f7d3ba2010-12-09 13:32:48 +0100843
Harald Weltec91085e2022-02-10 18:05:45 +0100844 # If we are not in batch mode we are done in any case, so lets
845 # exit here.
846 if not opts.batch_mode:
847 sys.exit(rc)
Philipp Maierc5b422e2019-08-30 11:41:02 +0200848
Harald Weltec91085e2022-02-10 18:05:45 +0100849 first = False