Skip to content

Commit

Permalink
add SIM/USIM communication via modem AT commands
Browse files Browse the repository at this point in the history
  • Loading branch information
Nementon committed Mar 2, 2024
1 parent 790a8fb commit c9c7346
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 13 deletions.
17 changes: 12 additions & 5 deletions card/ICC.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
from smartcard.util import toHexString

from card.utils import *

from .modem.modem_card_request import ModemCardRequest

###########################################################
# ISO7816 class with attributes and methods as defined
# by ISO-7816 part 4 standard for smartcard
Expand Down Expand Up @@ -141,19 +142,20 @@ class ISO7816(object):
0xAB : 'Security Attribute expanded',
}

def __init__(self, CLA=0x00, reader=''):
def __init__(self, CLA=0x00, reader='', modem_device_path=''):
"""
connect smartcard and defines class CLA code for communication
uses "pyscard" library services
creates self.CLA attribute with CLA code
and self.coms attribute with associated "apdu_stack" instance
"""

cardtype = AnyCardType()
if reader:
cardrequest = CardRequest(timeout=1, cardType=cardtype, readers=[reader])
if modem_device_path:
cardrequest = ModemCardRequest(modem_device_path, timeout=1, cardType=cardtype, readers=[reader])
else:
cardrequest = CardRequest(timeout=1, cardType=cardtype)
cardrequest = CardRequest(timeout=1, cardType=cardtype, readers=[reader])
self.cardservice = cardrequest.waitforcard()
self.cardservice.connection.connect()
self.reader = self.cardservice.connection.getReader()
Expand Down Expand Up @@ -1784,3 +1786,8 @@ def select_by_aid(self, aid_num=1):
if hasattr(self, 'AID') and aid_num <= len(self.AID)+1:
return self.select(self.AID[aid_num-1], 'aid')

def dispose(self):
try:
self.cardservice.dispose()
except:
pass
4 changes: 2 additions & 2 deletions card/SIM.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,12 @@ class SIM(ISO7816):
use self.dbg = 1 or more to print live debugging information
"""

def __init__(self, reader=''):
def __init__(self, reader='', modem_device_path=''):
"""
initialize like an ISO7816-4 card with CLA=0xA0
can also be used for USIM working in SIM mode,
"""
ISO7816.__init__(self, CLA=0xA0, reader=reader)
ISO7816.__init__(self, CLA=0xA0, reader=reader, modem_device_path=modem_device_path)
#
if self.dbg >= 2:
log(3, '(SIM.__init__) type definition: %s' % type(self))
Expand Down
4 changes: 2 additions & 2 deletions card/USIM.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,15 +177,15 @@ class USIM(UICC):
use self.dbg = 1 or more to print live debugging information
"""

def __init__(self, reader=''):
def __init__(self, reader='', modem_device_path=''):
"""
initializes like an ISO7816-4 card with CLA=0x00
and checks available AID (Application ID) read from EF_DIR
initializes on the MF
"""
# initialize like a UICC
ISO7816.__init__(self, CLA=0x00, reader=reader)
ISO7816.__init__(self, CLA=0x00, reader=reader, modem_device_path=modem_device_path)
self.AID = []
self.AID_GP = {}
self.AID_USIM = None
Expand Down
Empty file added card/modem/__init__.py
Empty file.
53 changes: 53 additions & 0 deletions card/modem/at_command_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import time
import serial
from typing import Optional, Union, Callable, Any

class ATCommandClient:

def __init__(self, device_path: str, timeout: Optional[float] = 1.0) -> None:
if timeout < 0.5:
timeout = 0.5

self._device_path = device_path
self._timeout = timeout
self._serial = None

def connect(self) -> None:
if self._serial:
return

self._serial = serial.Serial(
self._device_path,
115200,
timeout=0.001,
)

def transmit(self, at_command: Union[str, bytes], transform: Optional[Callable[[str, str], Any]] = lambda x,y: y) -> Union[str, Any]:
if not self._serial:
raise ValueError("Client shall be connected")

if isinstance(at_command, bytes):
at_command = at_command.decode()

if at_command[-2::] != "\r\n":
at_command += "\r\n"

at_command = at_command.encode()
self._serial.write(at_command)

resp = b''
read_until = time.time() + self._timeout
while b'OK' not in resp and b'ERROR' not in resp:
resp += self._serial.read(256)
if time.time() > read_until:
break

return transform(at_command, resp.decode())

def dispose(self) -> None:
if not self._serial:
return

self._serial.close()
self._serial = None

99 changes: 99 additions & 0 deletions card/modem/modem_card_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import logging
import time
from typing import Any, Iterable, Optional, List, Tuple
from smartcard.CardType import AnyCardType, CardType
from serial import SerialException
from .at_command_client import ATCommandClient

logger = logging.getLogger("modem")

class ModemCardRequest:
def __init__(self, modem_device_path, timeout: int = 1, cardType: CardType = AnyCardType, readers: Optional[Iterable[str]] = None) -> None:
self._readers = readers or ['']
self._timeout = timeout
self._client = ATCommandClient(modem_device_path, timeout=float(timeout*15))

@property
def connection(self) -> Any:
return self

def waitforcard(self) -> None:
self.connect()
return self

def connect(self) -> None:
self._client.connect()

def getReader(self) -> Any:
return self._readers

def getATR(self) -> Any:
return None

def transmit(self, apdu: List[int]) -> Any:
"""
Transmits SIM APDU to the modem.
"""

at_command = self._to_csim_command(apdu)
data, sw1, sw2 = [], 0xff, 0xff

attempt_until = time.time() + self._timeout
try:
while sw1 == 0xff and sw2 == 0xff:
data, sw1, sw2 = self._client.transmit(at_command, self._at_response_to_card_response)
except SerialException as e:
logger.debug("Serial communication error << {e} ... retrying")
if time.time() > attempt_until:
raise

logger.debug(f"""
APDU << {apdu}
AT Command << {at_command}
Ret << data:{data}, sw1:{sw1}, sw2:{sw2}
""")
return (data, sw1, sw2)

def _to_csim_command(self, apdu: List[int]) -> str:
"""
Transforms a SIM APDU represented as a list of integers (bytes data)
into its corresponding AT+CSIM command format.
"""

at_command = ("").join(map(lambda x: "%0.2X" % x, apdu))
at_command = f'AT+CSIM={len(at_command)},"{at_command}"'
return at_command

def _at_response_to_card_response(self, at_command: str, at_response: str) -> Tuple[List[int], int, int]:
"""
Transforms AT response to the expected CardService format.
"""

parts = list(filter(lambda x: x != '', at_response.split("\r\n")))
if len(parts) == 0:
return [], 0xff, 0xff # communication error

if not parts[-1] or 'ERROR' in parts[-1]:
return [], 0x6f, 0x0 # checking error: no precise diagnosis

res = parts[0]
res = res[res.find('"')+1:-1:]

return (
self._hexstream_to_bytes(res[:-4:]),
int(res[-4:-2:], 16),
int(res[-2::], 16)
)

def _hexstream_to_bytes(self, hexstream: str) -> List[int]:
"""
Returns a list of integers representing byte data from a hexadecimal stream.
"""

return list(
map(
lambda x: int(x, 16),
[hexstream[i:i+2] for i in range(0, len(hexstream), 2)]
)
)

9 changes: 5 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,18 @@

packages=[
"card"
],
],

# mandatory dependency
install_requires=[
'pyscard'
],
'pyscard',
'pyserial'
],

# optional dependency
extras_require={
'graph': ['pydot', 'graphviz']
},
},

author="Benoit Michau",
author_email="[email protected]",
Expand Down

0 comments on commit c9c7346

Please sign in to comment.