blob: a54a1b63a6cb47c7c8e4d3dadfddbd8e39dc2472 [file] [log] [blame]
Harald Welte531894d2023-07-11 19:11:11 +02001# coding=utf-8
2"""Representation of the runtime state of an application like pySim-shell.
3"""
4
5# (C) 2021 by Harald Welte <laforge@osmocom.org>
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20from typing import Optional, Tuple
21
22from pySim.utils import sw_match, h2b, i2h, is_hex, bertlv_parse_one, Hexstr
23from pySim.exceptions import *
24from pySim.filesystem import *
25
26def lchan_nr_from_cla(cla: int) -> int:
27 """Resolve the logical channel number from the CLA byte."""
28 # TS 102 221 10.1.1 Coding of Class Byte
29 if cla >> 4 in [0x0, 0xA, 0x8]:
30 # Table 10.3
31 return cla & 0x03
32 elif cla & 0xD0 in [0x40, 0xC0]:
33 # Table 10.4a
34 return 4 + (cla & 0x0F)
35 else:
36 raise ValueError('Could not determine logical channel for CLA=%2X' % cla)
37
38class RuntimeState:
39 """Represent the runtime state of a session with a card."""
40
Harald Welte49acc062023-10-21 20:21:51 +020041 def __init__(self, card: 'CardBase', profile: 'CardProfile'):
Harald Welte531894d2023-07-11 19:11:11 +020042 """
43 Args:
44 card : pysim.cards.Card instance
45 profile : CardProfile instance
46 """
47 self.mf = CardMF(profile=profile)
48 self.card = card
49 self.profile = profile
50 self.lchan = {}
51 # the basic logical channel always exists
52 self.lchan[0] = RuntimeLchan(0, self)
53
54 # make sure the class and selection control bytes, which are specified
55 # by the card profile are used
56 self.card.set_apdu_parameter(
57 cla=self.profile.cla, sel_ctrl=self.profile.sel_ctrl)
58
59 for addon_cls in self.profile.addons:
60 addon = addon_cls()
61 if addon.probe(self.card):
62 print("Detected %s Add-on \"%s\"" % (self.profile, addon))
63 for f in addon.files_in_mf:
64 self.mf.add_file(f)
65
66 # go back to MF before the next steps (addon probing might have changed DF)
67 self.card._scc.select_file('3F00')
68
69 # add application ADFs + MF-files from profile
70 apps = self._match_applications()
71 for a in apps:
72 if a.adf:
73 self.mf.add_application_df(a.adf)
74 for f in self.profile.files_in_mf:
75 self.mf.add_file(f)
76 self.conserve_write = True
77
78 # make sure that when the runtime state is created, the card is also
79 # in a defined state.
80 self.reset()
81
82 def _match_applications(self):
83 """match the applications from the profile with applications on the card"""
84 apps_profile = self.profile.applications
85
86 # When the profile does not feature any applications, then we are done already
87 if not apps_profile:
88 return []
89
90 # Read AIDs from card and match them against the applications defined by the
91 # card profile
92 aids_card = self.card.read_aids()
93 apps_taken = []
94 if aids_card:
95 aids_taken = []
96 print("AIDs on card:")
97 for a in aids_card:
98 for f in apps_profile:
99 if f.aid in a:
100 print(" %s: %s (EF.DIR)" % (f.name, a))
101 aids_taken.append(a)
102 apps_taken.append(f)
103 aids_unknown = set(aids_card) - set(aids_taken)
104 for a in aids_unknown:
105 print(" unknown: %s (EF.DIR)" % a)
106 else:
107 print("warning: EF.DIR seems to be empty!")
108
109 # Some card applications may not be registered in EF.DIR, we will actively
110 # probe for those applications
Philipp Maierd62182c2023-08-01 15:23:19 +0200111 for f in sorted(set(apps_profile) - set(apps_taken), key=str):
Harald Welte531894d2023-07-11 19:11:11 +0200112 try:
113 data, sw = self.card.select_adf_by_aid(f.aid)
114 if sw == "9000":
115 print(" %s: %s" % (f.name, f.aid))
116 apps_taken.append(f)
117 except (SwMatchError, ProtocolError):
118 pass
119 return apps_taken
120
121 def reset(self, cmd_app=None) -> Hexstr:
122 """Perform physical card reset and obtain ATR.
123 Args:
124 cmd_app : Command Application State (for unregistering old file commands)
125 """
126 # delete all lchan != 0 (basic lchan)
Philipp Maier92b93562023-07-21 11:38:26 +0200127 for lchan_nr in list(self.lchan.keys()):
Harald Welte531894d2023-07-11 19:11:11 +0200128 if lchan_nr == 0:
129 continue
130 del self.lchan[lchan_nr]
131 atr = i2h(self.card.reset())
132 # select MF to reset internal state and to verify card really works
133 self.lchan[0].select('MF', cmd_app)
134 self.lchan[0].selected_adf = None
135 return atr
136
137 def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan':
138 """Add a logical channel to the runtime state. You shouldn't call this
139 directly but always go through RuntimeLchan.add_lchan()."""
140 if lchan_nr in self.lchan.keys():
141 raise ValueError('Cannot create already-existing lchan %d' % lchan_nr)
142 self.lchan[lchan_nr] = RuntimeLchan(lchan_nr, self)
143 return self.lchan[lchan_nr]
144
145 def del_lchan(self, lchan_nr: int):
146 if lchan_nr in self.lchan.keys():
147 del self.lchan[lchan_nr]
148 return True
149 else:
150 return False
151
152 def get_lchan_by_cla(self, cla) -> Optional['RuntimeLchan']:
153 lchan_nr = lchan_nr_from_cla(cla)
154 if lchan_nr in self.lchan.keys():
155 return self.lchan[lchan_nr]
156 else:
157 return None
158
159
160class RuntimeLchan:
161 """Represent the runtime state of a logical channel with a card."""
162
163 def __init__(self, lchan_nr: int, rs: RuntimeState):
164 self.lchan_nr = lchan_nr
165 self.rs = rs
166 self.selected_file = self.rs.mf
167 self.selected_adf = None
168 self.selected_file_fcp = None
169 self.selected_file_fcp_hex = None
Harald Welte46255122023-10-21 23:40:42 +0200170 self.scc = self.rs.card._scc.fork_lchan(lchan_nr)
Harald Welte531894d2023-07-11 19:11:11 +0200171
172 def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan':
173 """Add a new logical channel from the current logical channel. Just affects
174 internal state, doesn't actually open a channel with the UICC."""
175 new_lchan = self.rs.add_lchan(lchan_nr)
176 # See TS 102 221 Table 8.3
177 if self.lchan_nr != 0:
178 new_lchan.selected_file = self.get_cwd()
179 new_lchan.selected_adf = self.selected_adf
180 return new_lchan
181
182 def selected_file_descriptor_byte(self) -> dict:
183 return self.selected_file_fcp['file_descriptor']['file_descriptor_byte']
184
185 def selected_file_shareable(self) -> bool:
186 return self.selected_file_descriptor_byte()['shareable']
187
188 def selected_file_structure(self) -> str:
189 return self.selected_file_descriptor_byte()['structure']
190
191 def selected_file_type(self) -> str:
192 return self.selected_file_descriptor_byte()['file_type']
193
194 def selected_file_num_of_rec(self) -> Optional[int]:
195 return self.selected_file_fcp['file_descriptor'].get('num_of_rec')
196
197 def get_cwd(self) -> CardDF:
198 """Obtain the current working directory.
199
200 Returns:
201 CardDF instance
202 """
203 if isinstance(self.selected_file, CardDF):
204 return self.selected_file
205 else:
206 return self.selected_file.parent
207
208 def get_application_df(self) -> Optional[CardADF]:
209 """Obtain the currently selected application DF (if any).
210
211 Returns:
212 CardADF() instance or None"""
213 # iterate upwards from selected file; check if any is an ADF
214 node = self.selected_file
215 while node.parent != node:
216 if isinstance(node, CardADF):
217 return node
218 node = node.parent
219 return None
220
221 def interpret_sw(self, sw: str):
222 """Interpret a given status word relative to the currently selected application
223 or the underlying card profile.
224
225 Args:
226 sw : Status word as string of 4 hex digits
227
228 Returns:
229 Tuple of two strings
230 """
231 res = None
232 adf = self.get_application_df()
233 if adf:
234 app = adf.application
235 # The application either comes with its own interpret_sw
236 # method or we will use the interpret_sw method from the
237 # card profile.
238 if app and hasattr(app, "interpret_sw"):
239 res = app.interpret_sw(sw)
240 return res or self.rs.profile.interpret_sw(sw)
241
242 def probe_file(self, fid: str, cmd_app=None):
243 """Blindly try to select a file and automatically add a matching file
244 object if the file actually exists."""
245 if not is_hex(fid, 4, 4):
246 raise ValueError(
247 "Cannot select unknown file by name %s, only hexadecimal 4 digit FID is allowed" % fid)
248
249 try:
Harald Welte46255122023-10-21 23:40:42 +0200250 (data, sw) = self.scc.select_file(fid)
Harald Welte531894d2023-07-11 19:11:11 +0200251 except SwMatchError as swm:
252 k = self.interpret_sw(swm.sw_actual)
253 if not k:
254 raise(swm)
255 raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1]))
256
257 select_resp = self.selected_file.decode_select_response(data)
258 if (select_resp['file_descriptor']['file_descriptor_byte']['file_type'] == 'df'):
259 f = CardDF(fid=fid, sfid=None, name="DF." + str(fid).upper(),
260 desc="dedicated file, manually added at runtime")
261 else:
262 if (select_resp['file_descriptor']['file_descriptor_byte']['structure'] == 'transparent'):
263 f = TransparentEF(fid=fid, sfid=None, name="EF." + str(fid).upper(),
264 desc="elementary file, manually added at runtime")
265 else:
266 f = LinFixedEF(fid=fid, sfid=None, name="EF." + str(fid).upper(),
267 desc="elementary file, manually added at runtime")
268
269 self.selected_file.add_files([f])
270 self.selected_file = f
271 return select_resp, data
272
273 def _select_pre(self, cmd_app):
274 # unregister commands of old file
275 if cmd_app and self.selected_file.shell_commands:
276 for c in self.selected_file.shell_commands:
277 cmd_app.unregister_command_set(c)
278
279 def _select_post(self, cmd_app):
280 # register commands of new file
281 if cmd_app and self.selected_file.shell_commands:
282 for c in self.selected_file.shell_commands:
283 cmd_app.register_command_set(c)
284
285 def select_file(self, file: CardFile, cmd_app=None):
286 """Select a file (EF, DF, ADF, MF, ...).
287
288 Args:
289 file : CardFile [or derived class] instance
290 cmd_app : Command Application State (for unregistering old file commands)
291 """
292 # we need to find a path from our self.selected_file to the destination
293 inter_path = self.selected_file.build_select_path_to(file)
294 if not inter_path:
295 raise RuntimeError('Cannot determine path from %s to %s' % (self.selected_file, file))
296
297 self._select_pre(cmd_app)
298
299 for p in inter_path:
300 try:
301 if isinstance(p, CardADF):
Harald Welte6dd6f3e2023-10-22 10:28:18 +0200302 (data, sw) = self.rs.card.select_adf_by_aid(p.aid, scc=self.scc)
Harald Welte531894d2023-07-11 19:11:11 +0200303 self.selected_adf = p
304 else:
Harald Welte46255122023-10-21 23:40:42 +0200305 (data, sw) = self.scc.select_file(p.fid)
Harald Welte531894d2023-07-11 19:11:11 +0200306 self.selected_file = p
307 except SwMatchError as swm:
308 self._select_post(cmd_app)
309 raise(swm)
310
311 self._select_post(cmd_app)
312
313 def select(self, name: str, cmd_app=None):
314 """Select a file (EF, DF, ADF, MF, ...).
315
316 Args:
317 name : Name of file to select
318 cmd_app : Command Application State (for unregistering old file commands)
319 """
320 # handling of entire paths with multiple directories/elements
321 if '/' in name:
322 prev_sel_file = self.selected_file
323 pathlist = name.split('/')
324 # treat /DF.GSM/foo like MF/DF.GSM/foo
325 if pathlist[0] == '':
326 pathlist[0] = 'MF'
327 try:
328 for p in pathlist:
329 self.select(p, cmd_app)
330 return
331 except Exception as e:
332 # if any intermediate step fails, go back to where we were
333 self.select_file(prev_sel_file, cmd_app)
334 raise e
335
336 sels = self.selected_file.get_selectables()
337 if is_hex(name):
338 name = name.lower()
339
340 self._select_pre(cmd_app)
341
342 if name in sels:
343 f = sels[name]
344 try:
345 if isinstance(f, CardADF):
Harald Welte6dd6f3e2023-10-22 10:28:18 +0200346 (data, sw) = self.rs.card.select_adf_by_aid(f.aid, scc=self.scc)
Harald Welte531894d2023-07-11 19:11:11 +0200347 else:
Harald Welte46255122023-10-21 23:40:42 +0200348 (data, sw) = self.scc.select_file(f.fid)
Harald Welte531894d2023-07-11 19:11:11 +0200349 self.selected_file = f
350 except SwMatchError as swm:
351 k = self.interpret_sw(swm.sw_actual)
352 if not k:
353 raise(swm)
354 raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1]))
355 select_resp = f.decode_select_response(data)
356 else:
357 (select_resp, data) = self.probe_file(name, cmd_app)
358
359 # store the raw + decoded FCP for later reference
360 self.selected_file_fcp_hex = data
361 self.selected_file_fcp = select_resp
362
363 self._select_post(cmd_app)
364 return select_resp
365
366 def status(self):
367 """Request STATUS (current selected file FCP) from card."""
Harald Welte46255122023-10-21 23:40:42 +0200368 (data, sw) = self.scc.status()
Harald Welte531894d2023-07-11 19:11:11 +0200369 return self.selected_file.decode_select_response(data)
370
371 def get_file_for_selectable(self, name: str):
372 sels = self.selected_file.get_selectables()
373 return sels[name]
374
375 def activate_file(self, name: str):
376 """Request ACTIVATE FILE of specified file."""
377 sels = self.selected_file.get_selectables()
378 f = sels[name]
Harald Welte46255122023-10-21 23:40:42 +0200379 data, sw = self.scc.activate_file(f.fid)
Harald Welte531894d2023-07-11 19:11:11 +0200380 return data, sw
381
382 def read_binary(self, length: int = None, offset: int = 0):
383 """Read [part of] a transparent EF binary data.
384
385 Args:
386 length : Amount of data to read (None: as much as possible)
387 offset : Offset into the file from which to read 'length' bytes
388 Returns:
389 binary data read from the file
390 """
391 if not isinstance(self.selected_file, TransparentEF):
392 raise TypeError("Only works with TransparentEF")
Harald Welte46255122023-10-21 23:40:42 +0200393 return self.scc.read_binary(self.selected_file.fid, length, offset)
Harald Welte531894d2023-07-11 19:11:11 +0200394
395 def read_binary_dec(self) -> Tuple[dict, str]:
396 """Read [part of] a transparent EF binary data and decode it.
397
398 Args:
399 length : Amount of data to read (None: as much as possible)
400 offset : Offset into the file from which to read 'length' bytes
401 Returns:
402 abstract decode data read from the file
403 """
404 (data, sw) = self.read_binary()
405 dec_data = self.selected_file.decode_hex(data)
406 return (dec_data, sw)
407
408 def update_binary(self, data_hex: str, offset: int = 0):
409 """Update transparent EF binary data.
410
411 Args:
412 data_hex : hex string of data to be written
413 offset : Offset into the file from which to write 'data_hex'
414 """
415 if not isinstance(self.selected_file, TransparentEF):
416 raise TypeError("Only works with TransparentEF")
Harald Welte46255122023-10-21 23:40:42 +0200417 return self.scc.update_binary(self.selected_file.fid, data_hex, offset, conserve=self.rs.conserve_write)
Harald Welte531894d2023-07-11 19:11:11 +0200418
419 def update_binary_dec(self, data: dict):
420 """Update transparent EF from abstract data. Encodes the data to binary and
421 then updates the EF with it.
422
423 Args:
424 data : abstract data which is to be encoded and written
425 """
426 data_hex = self.selected_file.encode_hex(data)
427 return self.update_binary(data_hex)
428
429 def read_record(self, rec_nr: int = 0):
430 """Read a record as binary data.
431
432 Args:
433 rec_nr : Record number to read
434 Returns:
435 hex string of binary data contained in record
436 """
437 if not isinstance(self.selected_file, LinFixedEF):
438 raise TypeError("Only works with Linear Fixed EF")
439 # returns a string of hex nibbles
Harald Welte46255122023-10-21 23:40:42 +0200440 return self.scc.read_record(self.selected_file.fid, rec_nr)
Harald Welte531894d2023-07-11 19:11:11 +0200441
442 def read_record_dec(self, rec_nr: int = 0) -> Tuple[dict, str]:
443 """Read a record and decode it to abstract data.
444
445 Args:
446 rec_nr : Record number to read
447 Returns:
448 abstract data contained in record
449 """
450 (data, sw) = self.read_record(rec_nr)
451 return (self.selected_file.decode_record_hex(data, rec_nr), sw)
452
453 def update_record(self, rec_nr: int, data_hex: str):
454 """Update a record with given binary data
455
456 Args:
457 rec_nr : Record number to read
458 data_hex : Hex string binary data to be written
459 """
460 if not isinstance(self.selected_file, LinFixedEF):
461 raise TypeError("Only works with Linear Fixed EF")
Harald Welte46255122023-10-21 23:40:42 +0200462 return self.scc.update_record(self.selected_file.fid, rec_nr, data_hex,
Philipp Maier37e57e02023-09-07 12:43:12 +0200463 conserve=self.rs.conserve_write,
464 leftpad=self.selected_file.leftpad)
Harald Welte531894d2023-07-11 19:11:11 +0200465
466 def update_record_dec(self, rec_nr: int, data: dict):
467 """Update a record with given abstract data. Will encode abstract to binary data
468 and then write it to the given record on the card.
469
470 Args:
471 rec_nr : Record number to read
472 data_hex : Abstract data to be written
473 """
474 data_hex = self.selected_file.encode_record_hex(data, rec_nr)
475 return self.update_record(rec_nr, data_hex)
476
477 def retrieve_data(self, tag: int = 0):
478 """Read a DO/TLV as binary data.
479
480 Args:
481 tag : Tag of TLV/DO to read
482 Returns:
483 hex string of full BER-TLV DO including Tag and Length
484 """
485 if not isinstance(self.selected_file, BerTlvEF):
486 raise TypeError("Only works with BER-TLV EF")
487 # returns a string of hex nibbles
Harald Welte46255122023-10-21 23:40:42 +0200488 return self.scc.retrieve_data(self.selected_file.fid, tag)
Harald Welte531894d2023-07-11 19:11:11 +0200489
490 def retrieve_tags(self):
491 """Retrieve tags available on BER-TLV EF.
492
493 Returns:
494 list of integer tags contained in EF
495 """
496 if not isinstance(self.selected_file, BerTlvEF):
497 raise TypeError("Only works with BER-TLV EF")
Harald Welte46255122023-10-21 23:40:42 +0200498 data, sw = self.scc.retrieve_data(self.selected_file.fid, 0x5c)
Harald Welte531894d2023-07-11 19:11:11 +0200499 tag, length, value, remainder = bertlv_parse_one(h2b(data))
500 return list(value)
501
502 def set_data(self, tag: int, data_hex: str):
503 """Update a TLV/DO with given binary data
504
505 Args:
506 tag : Tag of TLV/DO to be written
507 data_hex : Hex string binary data to be written (value portion)
508 """
509 if not isinstance(self.selected_file, BerTlvEF):
510 raise TypeError("Only works with BER-TLV EF")
Harald Welte46255122023-10-21 23:40:42 +0200511 return self.scc.set_data(self.selected_file.fid, tag, data_hex, conserve=self.rs.conserve_write)
Harald Welte531894d2023-07-11 19:11:11 +0200512
513 def unregister_cmds(self, cmd_app=None):
514 """Unregister all file specific commands."""
515 if cmd_app and self.selected_file.shell_commands:
516 for c in self.selected_file.shell_commands:
517 cmd_app.unregister_command_set(c)
518
519
520