diff --git a/pySim-prog.py b/pySim-prog.py index f707c57c..601f980c 100755 --- a/pySim-prog.py +++ b/pySim-prog.py @@ -61,6 +61,14 @@ def parse_options(): help="Which PC/SC reader number for SIM access", default=None, ) + parser.add_option("--modem-device", dest="modem_dev", metavar="DEV", + help="Serial port of modem for Generic SIM Access (3GPP TS 27.007)", + default=None, + ) + parser.add_option("--modem-baud", dest="modem_baud", type="int", metavar="BAUD", + help="Baudrate used for modem's port [default: %default]", + default=115200, + ) parser.add_option("--osmocon", dest="osmocon_sock", metavar="PATH", help="Socket path for Calypso (e.g. Motorola C1XX) based reader (via OsmocomBB)", default=None, diff --git a/pySim-read.py b/pySim-read.py index df21531f..e49a907a 100755 --- a/pySim-read.py +++ b/pySim-read.py @@ -53,6 +53,14 @@ def parse_options(): help="Which PC/SC reader number for SIM access", default=None, ) + parser.add_option("--modem-device", dest="modem_dev", metavar="DEV", + help="Serial port of modem for Generic SIM Access (3GPP TS 27.007)", + default=None, + ) + parser.add_option("--modem-baud", dest="modem_baud", type="int", metavar="BAUD", + help="Baudrate used for modem's port [default: %default]", + default=115200, + ) parser.add_option("--osmocon", dest="osmocon_sock", metavar="PATH", help="Socket path for Calypso (e.g. Motorola C1XX) based reader (via OsmocomBB)", default=None, diff --git a/pySim/transport/modem_atcmd.py b/pySim/transport/modem_atcmd.py new file mode 100644 index 00000000..742ae8d4 --- /dev/null +++ b/pySim/transport/modem_atcmd.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" pySim: Transport Link for 3GPP TS 27.007 compliant modems +""" + +# Copyright (C) 2020 Vadim Yanitskiy +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from __future__ import absolute_import + +import logging as log +import serial +import time +import re + +from pySim.transport import LinkBase +from pySim.exceptions import * + +# HACK: if somebody needs to debug this thing +# log.root.setLevel(log.DEBUG) + +class ModemATCommandLink(LinkBase): + def __init__(self, device='/dev/ttyUSB0', baudrate=115200): + self._sl = serial.Serial(device, baudrate, timeout=5) + self._device = device + self._atr = None + + # Trigger initial reset + self.reset_card() + + def __del__(self): + self._sl.close() + + def send_at_cmd(self, cmd): + # Convert from string to bytes, if needed + bcmd = cmd if type(cmd) is bytes else cmd.encode() + bcmd += b'\r' + + # Send command to the modem + log.debug('Sending AT command: %s' % cmd) + try: + wlen = self._sl.write(bcmd) + assert(wlen == len(bcmd)) + except: + raise ReaderError('Failed to send AT command: %s' % cmd) + + # Give the modem some time... + time.sleep(0.3) + + # Read the response + try: + # Skip characters sent back + self._sl.read(wlen) + # Read the rest + rsp = self._sl.read_all() + + # Strip '\r\n' + rsp = rsp.strip() + # Split into a list + rsp = rsp.split(b'\r\n\r\n') + except: + raise ReaderError('Failed parse response to AT command: %s' % cmd) + + log.debug('Got response from modem: %s' % rsp) + return rsp + + def reset_card(self): + # Make sure that we can talk to the modem + if self.send_at_cmd('AT') != [b'OK']: + raise ReaderError('Failed to connect to modem') + + # Reset the modem, just to be sure + if self.send_at_cmd('ATZ') != [b'OK']: + raise ReaderError('Failed to reset the modem') + + # Make sure that generic SIM access is supported + if self.send_at_cmd('AT+CSIM=?') != [b'OK']: + raise ReaderError('The modem does not seem to support SIM access') + + log.info('Modem at \'%s\' is ready!' % self._device) + + def connect(self): + pass # Nothing to do really ... + + def disconnect(self): + pass # Nothing to do really ... + + def wait_for_card(self, timeout=None, newcardonly=False): + pass # Nothing to do really ... + + def send_apdu_raw(self, pdu): + # Prepare the command as described in 8.17 + cmd = 'AT+CSIM=%d,\"%s\"' % (len(pdu), pdu) + + # Send AT+CSIM command to the modem + # TODO: also handle +CME ERROR: + rsp = self.send_at_cmd(cmd) + if len(rsp) != 2 or rsp[-1] != b'OK': + raise ReaderError('APDU transfer failed: %s' % str(rsp)) + rsp = rsp[0] # Get rid of b'OK' + + # Make sure that the response has format: b'+CSIM: %d,\"%s\"' + try: + result = re.match(b'\+CSIM: (\d+),\"([0-9A-F]+)\"', rsp) + (rsp_pdu_len, rsp_pdu) = result.groups() + except: + raise ReaderError('Failed to parse response from modem: %s' % rsp) + + # TODO: make sure we have at least SW + data = rsp_pdu[:-4].decode() + sw = rsp_pdu[-4:].decode() + return data, sw diff --git a/pySim/utils.py b/pySim/utils.py index 20eb5a83..496b918a 100644 --- a/pySim/utils.py +++ b/pySim/utils.py @@ -485,6 +485,10 @@ def init_reader(opts): print("Using Calypso-based (OsmocomBB) reader interface") from pySim.transport.calypso import CalypsoSimLink sl = CalypsoSimLink(sock_path=opts.osmocon_sock) + elif opts.modem_dev is not None: + print("Using modem for Generic SIM Access (3GPP TS 27.007)") + from pySim.transport.modem_atcmd import ModemATCommandLink + sl = ModemATCommandLink(device=opts.modem_dev, baudrate=opts.modem_baud) else: # Serial reader is default print("Using serial reader interface") from pySim.transport.serial import SerialSimLink