Skip to content

Commit c9c7346

Browse files
committed
add SIM/USIM communication via modem AT commands
1 parent 790a8fb commit c9c7346

File tree

7 files changed

+173
-13
lines changed

7 files changed

+173
-13
lines changed

card/ICC.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@
4242
from smartcard.util import toHexString
4343

4444
from card.utils import *
45-
45+
from .modem.modem_card_request import ModemCardRequest
46+
4647
###########################################################
4748
# ISO7816 class with attributes and methods as defined
4849
# by ISO-7816 part 4 standard for smartcard
@@ -141,19 +142,20 @@ class ISO7816(object):
141142
0xAB : 'Security Attribute expanded',
142143
}
143144

144-
def __init__(self, CLA=0x00, reader=''):
145+
def __init__(self, CLA=0x00, reader='', modem_device_path=''):
145146
"""
146147
connect smartcard and defines class CLA code for communication
147148
uses "pyscard" library services
148149
149150
creates self.CLA attribute with CLA code
150151
and self.coms attribute with associated "apdu_stack" instance
151152
"""
153+
152154
cardtype = AnyCardType()
153-
if reader:
154-
cardrequest = CardRequest(timeout=1, cardType=cardtype, readers=[reader])
155+
if modem_device_path:
156+
cardrequest = ModemCardRequest(modem_device_path, timeout=1, cardType=cardtype, readers=[reader])
155157
else:
156-
cardrequest = CardRequest(timeout=1, cardType=cardtype)
158+
cardrequest = CardRequest(timeout=1, cardType=cardtype, readers=[reader])
157159
self.cardservice = cardrequest.waitforcard()
158160
self.cardservice.connection.connect()
159161
self.reader = self.cardservice.connection.getReader()
@@ -1784,3 +1786,8 @@ def select_by_aid(self, aid_num=1):
17841786
if hasattr(self, 'AID') and aid_num <= len(self.AID)+1:
17851787
return self.select(self.AID[aid_num-1], 'aid')
17861788

1789+
def dispose(self):
1790+
try:
1791+
self.cardservice.dispose()
1792+
except:
1793+
pass

card/SIM.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,12 @@ class SIM(ISO7816):
100100
use self.dbg = 1 or more to print live debugging information
101101
"""
102102

103-
def __init__(self, reader=''):
103+
def __init__(self, reader='', modem_device_path=''):
104104
"""
105105
initialize like an ISO7816-4 card with CLA=0xA0
106106
can also be used for USIM working in SIM mode,
107107
"""
108-
ISO7816.__init__(self, CLA=0xA0, reader=reader)
108+
ISO7816.__init__(self, CLA=0xA0, reader=reader, modem_device_path=modem_device_path)
109109
#
110110
if self.dbg >= 2:
111111
log(3, '(SIM.__init__) type definition: %s' % type(self))

card/USIM.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,15 +177,15 @@ class USIM(UICC):
177177
use self.dbg = 1 or more to print live debugging information
178178
"""
179179

180-
def __init__(self, reader=''):
180+
def __init__(self, reader='', modem_device_path=''):
181181
"""
182182
initializes like an ISO7816-4 card with CLA=0x00
183183
and checks available AID (Application ID) read from EF_DIR
184184
185185
initializes on the MF
186186
"""
187187
# initialize like a UICC
188-
ISO7816.__init__(self, CLA=0x00, reader=reader)
188+
ISO7816.__init__(self, CLA=0x00, reader=reader, modem_device_path=modem_device_path)
189189
self.AID = []
190190
self.AID_GP = {}
191191
self.AID_USIM = None

card/modem/__init__.py

Whitespace-only changes.

card/modem/at_command_client.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import time
2+
import serial
3+
from typing import Optional, Union, Callable, Any
4+
5+
class ATCommandClient:
6+
7+
def __init__(self, device_path: str, timeout: Optional[float] = 1.0) -> None:
8+
if timeout < 0.5:
9+
timeout = 0.5
10+
11+
self._device_path = device_path
12+
self._timeout = timeout
13+
self._serial = None
14+
15+
def connect(self) -> None:
16+
if self._serial:
17+
return
18+
19+
self._serial = serial.Serial(
20+
self._device_path,
21+
115200,
22+
timeout=0.001,
23+
)
24+
25+
def transmit(self, at_command: Union[str, bytes], transform: Optional[Callable[[str, str], Any]] = lambda x,y: y) -> Union[str, Any]:
26+
if not self._serial:
27+
raise ValueError("Client shall be connected")
28+
29+
if isinstance(at_command, bytes):
30+
at_command = at_command.decode()
31+
32+
if at_command[-2::] != "\r\n":
33+
at_command += "\r\n"
34+
35+
at_command = at_command.encode()
36+
self._serial.write(at_command)
37+
38+
resp = b''
39+
read_until = time.time() + self._timeout
40+
while b'OK' not in resp and b'ERROR' not in resp:
41+
resp += self._serial.read(256)
42+
if time.time() > read_until:
43+
break
44+
45+
return transform(at_command, resp.decode())
46+
47+
def dispose(self) -> None:
48+
if not self._serial:
49+
return
50+
51+
self._serial.close()
52+
self._serial = None
53+

card/modem/modem_card_request.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import logging
2+
import time
3+
from typing import Any, Iterable, Optional, List, Tuple
4+
from smartcard.CardType import AnyCardType, CardType
5+
from serial import SerialException
6+
from .at_command_client import ATCommandClient
7+
8+
logger = logging.getLogger("modem")
9+
10+
class ModemCardRequest:
11+
def __init__(self, modem_device_path, timeout: int = 1, cardType: CardType = AnyCardType, readers: Optional[Iterable[str]] = None) -> None:
12+
self._readers = readers or ['']
13+
self._timeout = timeout
14+
self._client = ATCommandClient(modem_device_path, timeout=float(timeout*15))
15+
16+
@property
17+
def connection(self) -> Any:
18+
return self
19+
20+
def waitforcard(self) -> None:
21+
self.connect()
22+
return self
23+
24+
def connect(self) -> None:
25+
self._client.connect()
26+
27+
def getReader(self) -> Any:
28+
return self._readers
29+
30+
def getATR(self) -> Any:
31+
return None
32+
33+
def transmit(self, apdu: List[int]) -> Any:
34+
"""
35+
Transmits SIM APDU to the modem.
36+
"""
37+
38+
at_command = self._to_csim_command(apdu)
39+
data, sw1, sw2 = [], 0xff, 0xff
40+
41+
attempt_until = time.time() + self._timeout
42+
try:
43+
while sw1 == 0xff and sw2 == 0xff:
44+
data, sw1, sw2 = self._client.transmit(at_command, self._at_response_to_card_response)
45+
except SerialException as e:
46+
logger.debug("Serial communication error << {e} ... retrying")
47+
if time.time() > attempt_until:
48+
raise
49+
50+
logger.debug(f"""
51+
APDU << {apdu}
52+
AT Command << {at_command}
53+
Ret << data:{data}, sw1:{sw1}, sw2:{sw2}
54+
""")
55+
return (data, sw1, sw2)
56+
57+
def _to_csim_command(self, apdu: List[int]) -> str:
58+
"""
59+
Transforms a SIM APDU represented as a list of integers (bytes data)
60+
into its corresponding AT+CSIM command format.
61+
"""
62+
63+
at_command = ("").join(map(lambda x: "%0.2X" % x, apdu))
64+
at_command = f'AT+CSIM={len(at_command)},"{at_command}"'
65+
return at_command
66+
67+
def _at_response_to_card_response(self, at_command: str, at_response: str) -> Tuple[List[int], int, int]:
68+
"""
69+
Transforms AT response to the expected CardService format.
70+
"""
71+
72+
parts = list(filter(lambda x: x != '', at_response.split("\r\n")))
73+
if len(parts) == 0:
74+
return [], 0xff, 0xff # communication error
75+
76+
if not parts[-1] or 'ERROR' in parts[-1]:
77+
return [], 0x6f, 0x0 # checking error: no precise diagnosis
78+
79+
res = parts[0]
80+
res = res[res.find('"')+1:-1:]
81+
82+
return (
83+
self._hexstream_to_bytes(res[:-4:]),
84+
int(res[-4:-2:], 16),
85+
int(res[-2::], 16)
86+
)
87+
88+
def _hexstream_to_bytes(self, hexstream: str) -> List[int]:
89+
"""
90+
Returns a list of integers representing byte data from a hexadecimal stream.
91+
"""
92+
93+
return list(
94+
map(
95+
lambda x: int(x, 16),
96+
[hexstream[i:i+2] for i in range(0, len(hexstream), 2)]
97+
)
98+
)
99+

setup.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,18 @@
66

77
packages=[
88
"card"
9-
],
9+
],
1010

1111
# mandatory dependency
1212
install_requires=[
13-
'pyscard'
14-
],
13+
'pyscard',
14+
'pyserial'
15+
],
1516

1617
# optional dependency
1718
extras_require={
1819
'graph': ['pydot', 'graphviz']
19-
},
20+
},
2021

2122
author="Benoit Michau",
2223
author_email="[email protected]",

0 commit comments

Comments
 (0)