blob: f836ec80cc35d134975bc9c56ef7a122d79509af [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)
Philipp Maierb8b61bf2023-12-07 10:39:21 +010067 self.lchan[0].select('MF')
Harald Welte531894d2023-07-11 19:11:11 +020068
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:
Philipp Maiere30456b2023-12-07 11:07:55 +0100113 # we can not use the lchan provided methods select, or select_file
114 # since those method work on an already finished file model. At
115 # this point we are still in the initialization process, so it is
116 # no problem when we access the card object directly without caring
117 # about updating other states. For normal selects at runtime, the
118 # caller must use the lchan provided methods select or select_file!
Harald Welte531894d2023-07-11 19:11:11 +0200119 data, sw = self.card.select_adf_by_aid(f.aid)
Philipp Maier578cf122023-10-25 18:02:41 +0200120 self.selected_adf = f
Harald Welte531894d2023-07-11 19:11:11 +0200121 if sw == "9000":
122 print(" %s: %s" % (f.name, f.aid))
123 apps_taken.append(f)
124 except (SwMatchError, ProtocolError):
125 pass
126 return apps_taken
127
128 def reset(self, cmd_app=None) -> Hexstr:
129 """Perform physical card reset and obtain ATR.
130 Args:
131 cmd_app : Command Application State (for unregistering old file commands)
132 """
133 # delete all lchan != 0 (basic lchan)
Philipp Maier92b93562023-07-21 11:38:26 +0200134 for lchan_nr in list(self.lchan.keys()):
Harald Welte531894d2023-07-11 19:11:11 +0200135 if lchan_nr == 0:
136 continue
137 del self.lchan[lchan_nr]
138 atr = i2h(self.card.reset())
139 # select MF to reset internal state and to verify card really works
140 self.lchan[0].select('MF', cmd_app)
141 self.lchan[0].selected_adf = None
142 return atr
143
144 def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan':
145 """Add a logical channel to the runtime state. You shouldn't call this
146 directly but always go through RuntimeLchan.add_lchan()."""
147 if lchan_nr in self.lchan.keys():
148 raise ValueError('Cannot create already-existing lchan %d' % lchan_nr)
149 self.lchan[lchan_nr] = RuntimeLchan(lchan_nr, self)
150 return self.lchan[lchan_nr]
151
152 def del_lchan(self, lchan_nr: int):
153 if lchan_nr in self.lchan.keys():
154 del self.lchan[lchan_nr]
155 return True
156 else:
157 return False
158
159 def get_lchan_by_cla(self, cla) -> Optional['RuntimeLchan']:
160 lchan_nr = lchan_nr_from_cla(cla)
161 if lchan_nr in self.lchan.keys():
162 return self.lchan[lchan_nr]
163 else:
164 return None
165
166
167class RuntimeLchan:
168 """Represent the runtime state of a logical channel with a card."""
169
170 def __init__(self, lchan_nr: int, rs: RuntimeState):
171 self.lchan_nr = lchan_nr
172 self.rs = rs
Philipp Maierc038ccc2023-12-07 11:12:08 +0100173 self.scc = self.rs.card._scc.fork_lchan(lchan_nr)
174
175 # File reference data
Harald Welte531894d2023-07-11 19:11:11 +0200176 self.selected_file = self.rs.mf
177 self.selected_adf = None
178 self.selected_file_fcp = None
179 self.selected_file_fcp_hex = None
180
181 def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan':
182 """Add a new logical channel from the current logical channel. Just affects
183 internal state, doesn't actually open a channel with the UICC."""
184 new_lchan = self.rs.add_lchan(lchan_nr)
185 # See TS 102 221 Table 8.3
186 if self.lchan_nr != 0:
187 new_lchan.selected_file = self.get_cwd()
188 new_lchan.selected_adf = self.selected_adf
189 return new_lchan
190
191 def selected_file_descriptor_byte(self) -> dict:
192 return self.selected_file_fcp['file_descriptor']['file_descriptor_byte']
193
194 def selected_file_shareable(self) -> bool:
195 return self.selected_file_descriptor_byte()['shareable']
196
197 def selected_file_structure(self) -> str:
198 return self.selected_file_descriptor_byte()['structure']
199
200 def selected_file_type(self) -> str:
201 return self.selected_file_descriptor_byte()['file_type']
202
203 def selected_file_num_of_rec(self) -> Optional[int]:
204 return self.selected_file_fcp['file_descriptor'].get('num_of_rec')
205
206 def get_cwd(self) -> CardDF:
207 """Obtain the current working directory.
208
209 Returns:
210 CardDF instance
211 """
212 if isinstance(self.selected_file, CardDF):
213 return self.selected_file
214 else:
215 return self.selected_file.parent
216
217 def get_application_df(self) -> Optional[CardADF]:
218 """Obtain the currently selected application DF (if any).
219
220 Returns:
221 CardADF() instance or None"""
222 # iterate upwards from selected file; check if any is an ADF
223 node = self.selected_file
224 while node.parent != node:
225 if isinstance(node, CardADF):
226 return node
227 node = node.parent
228 return None
229
230 def interpret_sw(self, sw: str):
231 """Interpret a given status word relative to the currently selected application
232 or the underlying card profile.
233
234 Args:
235 sw : Status word as string of 4 hex digits
236
237 Returns:
238 Tuple of two strings
239 """
240 res = None
241 adf = self.get_application_df()
242 if adf:
243 app = adf.application
244 # The application either comes with its own interpret_sw
245 # method or we will use the interpret_sw method from the
246 # card profile.
247 if app and hasattr(app, "interpret_sw"):
248 res = app.interpret_sw(sw)
249 return res or self.rs.profile.interpret_sw(sw)
250
251 def probe_file(self, fid: str, cmd_app=None):
252 """Blindly try to select a file and automatically add a matching file
Philipp Maier1da86362023-10-31 13:17:14 +0100253 object if the file actually exists."""
Harald Welte531894d2023-07-11 19:11:11 +0200254 if not is_hex(fid, 4, 4):
255 raise ValueError(
256 "Cannot select unknown file by name %s, only hexadecimal 4 digit FID is allowed" % fid)
257
Philipp Maier82cc7cc2023-12-07 12:20:07 +0100258 self._select_pre(cmd_app)
259
Harald Welte531894d2023-07-11 19:11:11 +0200260 try:
Philipp Maier174fd322023-12-07 11:28:08 +0100261 # We access the card through the select_file method of the scc object.
262 # If we succeed, we know that the file exists on the card and we may
263 # proceed with creating a new CardEF object in the local file model at
264 # run time. In case the file does not exist on the card, we just abort.
265 # The state on the card (selected file/application) wont't be changed,
266 # so we do not have to update any state in that case.
Harald Welte46255122023-10-21 23:40:42 +0200267 (data, sw) = self.scc.select_file(fid)
Harald Welte531894d2023-07-11 19:11:11 +0200268 except SwMatchError as swm:
Philipp Maier82cc7cc2023-12-07 12:20:07 +0100269 self._select_post(cmd_app)
Harald Welte531894d2023-07-11 19:11:11 +0200270 k = self.interpret_sw(swm.sw_actual)
271 if not k:
272 raise(swm)
273 raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1]))
274
275 select_resp = self.selected_file.decode_select_response(data)
276 if (select_resp['file_descriptor']['file_descriptor_byte']['file_type'] == 'df'):
277 f = CardDF(fid=fid, sfid=None, name="DF." + str(fid).upper(),
278 desc="dedicated file, manually added at runtime")
279 else:
280 if (select_resp['file_descriptor']['file_descriptor_byte']['structure'] == 'transparent'):
281 f = TransparentEF(fid=fid, sfid=None, name="EF." + str(fid).upper(),
282 desc="elementary file, manually added at runtime")
283 else:
284 f = LinFixedEF(fid=fid, sfid=None, name="EF." + str(fid).upper(),
285 desc="elementary file, manually added at runtime")
286
287 self.selected_file.add_files([f])
Philipp Maier82cc7cc2023-12-07 12:20:07 +0100288
289 self._select_post(cmd_app, f, data)
Harald Welte531894d2023-07-11 19:11:11 +0200290
291 def _select_pre(self, cmd_app):
292 # unregister commands of old file
293 if cmd_app and self.selected_file.shell_commands:
294 for c in self.selected_file.shell_commands:
295 cmd_app.unregister_command_set(c)
296
Philipp Maier82cc7cc2023-12-07 12:20:07 +0100297 def _select_post(self, cmd_app, file:Optional[CardFile] = None, select_resp_data = None):
298 # we store some reference data (see above) about the currently selected file.
299 # This data must be updated after every select.
300 if file:
301 self.selected_file = file
302 if isinstance(file, CardADF):
303 self.selected_adf = file
304 if select_resp_data:
305 self.selected_file_fcp_hex = select_resp_data
306 self.selected_file_fcp = self.selected_file.decode_select_response(select_resp_data)
307
Harald Welte531894d2023-07-11 19:11:11 +0200308 # register commands of new file
309 if cmd_app and self.selected_file.shell_commands:
310 for c in self.selected_file.shell_commands:
311 cmd_app.register_command_set(c)
312
313 def select_file(self, file: CardFile, cmd_app=None):
314 """Select a file (EF, DF, ADF, MF, ...).
315
316 Args:
317 file : CardFile [or derived class] instance
318 cmd_app : Command Application State (for unregistering old file commands)
319 """
320 # we need to find a path from our self.selected_file to the destination
321 inter_path = self.selected_file.build_select_path_to(file)
322 if not inter_path:
323 raise RuntimeError('Cannot determine path from %s to %s' % (self.selected_file, file))
Harald Welte531894d2023-07-11 19:11:11 +0200324 self._select_pre(cmd_app)
325
Philipp Maier82cc7cc2023-12-07 12:20:07 +0100326 # be sure the variables that we pass to _select_post contain valid values.
327 selected_file = self.selected_file
328 data = self.selected_file_fcp_hex
329
330 for f in inter_path:
Harald Welte531894d2023-07-11 19:11:11 +0200331 try:
Philipp Maier82cc7cc2023-12-07 12:20:07 +0100332 # We now directly accessing the card to perform the selection. This
333 # will change the state of the card, so we must take care to update
334 # the local state (lchan) as well. This is done in the method
335 # _select_post. It should be noted that the caller must always use
336 # the methods select_file or select. The caller must not access the
337 # card directly since this would lead into an incoherence of the
338 # card state and the state of the lchan.
339 if isinstance(f, CardADF):
340 (data, sw) = self.rs.card.select_adf_by_aid(f.aid, scc=self.scc)
Harald Welte531894d2023-07-11 19:11:11 +0200341 else:
Philipp Maier82cc7cc2023-12-07 12:20:07 +0100342 (data, sw) = self.scc.select_file(f.fid)
343 selected_file = f
Harald Welte531894d2023-07-11 19:11:11 +0200344 except SwMatchError as swm:
Philipp Maier82cc7cc2023-12-07 12:20:07 +0100345 self._select_post(cmd_app, selected_file, data)
Harald Welte531894d2023-07-11 19:11:11 +0200346 raise(swm)
347
Philipp Maier82cc7cc2023-12-07 12:20:07 +0100348 self._select_post(cmd_app, f, data)
Harald Welte531894d2023-07-11 19:11:11 +0200349
350 def select(self, name: str, cmd_app=None):
351 """Select a file (EF, DF, ADF, MF, ...).
352
353 Args:
354 name : Name of file to select
355 cmd_app : Command Application State (for unregistering old file commands)
356 """
Philipp Maier82cc7cc2023-12-07 12:20:07 +0100357 # if any intermediate step fails, we must be able to go back where we were
358 prev_sel_file = self.selected_file
359
Harald Welte531894d2023-07-11 19:11:11 +0200360 # handling of entire paths with multiple directories/elements
361 if '/' in name:
Harald Welte531894d2023-07-11 19:11:11 +0200362 pathlist = name.split('/')
363 # treat /DF.GSM/foo like MF/DF.GSM/foo
364 if pathlist[0] == '':
365 pathlist[0] = 'MF'
366 try:
367 for p in pathlist:
368 self.select(p, cmd_app)
Philipp Maier82cc7cc2023-12-07 12:20:07 +0100369 return self.selected_file_fcp
Harald Welte531894d2023-07-11 19:11:11 +0200370 except Exception as e:
Harald Welte531894d2023-07-11 19:11:11 +0200371 self.select_file(prev_sel_file, cmd_app)
372 raise e
373
Philipp Maier82cc7cc2023-12-07 12:20:07 +0100374 # we are now in the directory where the target file is located
375 # so we can now refer to the get_selectables() method to get the
376 # file object and select it using select_file()
Harald Welte531894d2023-07-11 19:11:11 +0200377 sels = self.selected_file.get_selectables()
378 if is_hex(name):
379 name = name.lower()
380
Philipp Maier82cc7cc2023-12-07 12:20:07 +0100381 try:
382 if name in sels:
383 self.select_file(sels[name], cmd_app)
384 else:
385 self.probe_file(name, cmd_app)
386 except Exception as e:
387 self.select_file(prev_sel_file, cmd_app)
388 raise e
Harald Welte531894d2023-07-11 19:11:11 +0200389
Philipp Maier82cc7cc2023-12-07 12:20:07 +0100390 return self.selected_file_fcp
Harald Welte531894d2023-07-11 19:11:11 +0200391
392 def status(self):
393 """Request STATUS (current selected file FCP) from card."""
Harald Welte46255122023-10-21 23:40:42 +0200394 (data, sw) = self.scc.status()
Harald Welte531894d2023-07-11 19:11:11 +0200395 return self.selected_file.decode_select_response(data)
396
397 def get_file_for_selectable(self, name: str):
398 sels = self.selected_file.get_selectables()
399 return sels[name]
400
401 def activate_file(self, name: str):
402 """Request ACTIVATE FILE of specified file."""
403 sels = self.selected_file.get_selectables()
404 f = sels[name]
Harald Welte46255122023-10-21 23:40:42 +0200405 data, sw = self.scc.activate_file(f.fid)
Harald Welte531894d2023-07-11 19:11:11 +0200406 return data, sw
407
408 def read_binary(self, length: int = None, offset: int = 0):
409 """Read [part of] a transparent EF binary data.
410
411 Args:
412 length : Amount of data to read (None: as much as possible)
413 offset : Offset into the file from which to read 'length' bytes
414 Returns:
415 binary data read from the file
416 """
417 if not isinstance(self.selected_file, TransparentEF):
418 raise TypeError("Only works with TransparentEF")
Harald Welte46255122023-10-21 23:40:42 +0200419 return self.scc.read_binary(self.selected_file.fid, length, offset)
Harald Welte531894d2023-07-11 19:11:11 +0200420
421 def read_binary_dec(self) -> Tuple[dict, str]:
422 """Read [part of] a transparent EF binary data and decode it.
423
424 Args:
425 length : Amount of data to read (None: as much as possible)
426 offset : Offset into the file from which to read 'length' bytes
427 Returns:
428 abstract decode data read from the file
429 """
430 (data, sw) = self.read_binary()
431 dec_data = self.selected_file.decode_hex(data)
432 return (dec_data, sw)
433
434 def update_binary(self, data_hex: str, offset: int = 0):
435 """Update transparent EF binary data.
436
437 Args:
438 data_hex : hex string of data to be written
439 offset : Offset into the file from which to write 'data_hex'
440 """
441 if not isinstance(self.selected_file, TransparentEF):
442 raise TypeError("Only works with TransparentEF")
Harald Welte46255122023-10-21 23:40:42 +0200443 return self.scc.update_binary(self.selected_file.fid, data_hex, offset, conserve=self.rs.conserve_write)
Harald Welte531894d2023-07-11 19:11:11 +0200444
445 def update_binary_dec(self, data: dict):
446 """Update transparent EF from abstract data. Encodes the data to binary and
447 then updates the EF with it.
448
449 Args:
450 data : abstract data which is to be encoded and written
451 """
452 data_hex = self.selected_file.encode_hex(data)
453 return self.update_binary(data_hex)
454
455 def read_record(self, rec_nr: int = 0):
456 """Read a record as binary data.
457
458 Args:
459 rec_nr : Record number to read
460 Returns:
461 hex string of binary data contained in record
462 """
463 if not isinstance(self.selected_file, LinFixedEF):
464 raise TypeError("Only works with Linear Fixed EF")
465 # returns a string of hex nibbles
Harald Welte46255122023-10-21 23:40:42 +0200466 return self.scc.read_record(self.selected_file.fid, rec_nr)
Harald Welte531894d2023-07-11 19:11:11 +0200467
468 def read_record_dec(self, rec_nr: int = 0) -> Tuple[dict, str]:
469 """Read a record and decode it to abstract data.
470
471 Args:
472 rec_nr : Record number to read
473 Returns:
474 abstract data contained in record
475 """
476 (data, sw) = self.read_record(rec_nr)
477 return (self.selected_file.decode_record_hex(data, rec_nr), sw)
478
479 def update_record(self, rec_nr: int, data_hex: str):
480 """Update a record with given binary data
481
482 Args:
483 rec_nr : Record number to read
484 data_hex : Hex string binary data to be written
485 """
486 if not isinstance(self.selected_file, LinFixedEF):
487 raise TypeError("Only works with Linear Fixed EF")
Harald Welte46255122023-10-21 23:40:42 +0200488 return self.scc.update_record(self.selected_file.fid, rec_nr, data_hex,
Philipp Maier37e57e02023-09-07 12:43:12 +0200489 conserve=self.rs.conserve_write,
490 leftpad=self.selected_file.leftpad)
Harald Welte531894d2023-07-11 19:11:11 +0200491
492 def update_record_dec(self, rec_nr: int, data: dict):
493 """Update a record with given abstract data. Will encode abstract to binary data
494 and then write it to the given record on the card.
495
496 Args:
497 rec_nr : Record number to read
498 data_hex : Abstract data to be written
499 """
500 data_hex = self.selected_file.encode_record_hex(data, rec_nr)
501 return self.update_record(rec_nr, data_hex)
502
503 def retrieve_data(self, tag: int = 0):
504 """Read a DO/TLV as binary data.
505
506 Args:
507 tag : Tag of TLV/DO to read
508 Returns:
509 hex string of full BER-TLV DO including Tag and Length
510 """
511 if not isinstance(self.selected_file, BerTlvEF):
512 raise TypeError("Only works with BER-TLV EF")
513 # returns a string of hex nibbles
Harald Welte46255122023-10-21 23:40:42 +0200514 return self.scc.retrieve_data(self.selected_file.fid, tag)
Harald Welte531894d2023-07-11 19:11:11 +0200515
516 def retrieve_tags(self):
517 """Retrieve tags available on BER-TLV EF.
518
519 Returns:
520 list of integer tags contained in EF
521 """
522 if not isinstance(self.selected_file, BerTlvEF):
523 raise TypeError("Only works with BER-TLV EF")
Harald Welte46255122023-10-21 23:40:42 +0200524 data, sw = self.scc.retrieve_data(self.selected_file.fid, 0x5c)
Harald Welte531894d2023-07-11 19:11:11 +0200525 tag, length, value, remainder = bertlv_parse_one(h2b(data))
526 return list(value)
527
528 def set_data(self, tag: int, data_hex: str):
529 """Update a TLV/DO with given binary data
530
531 Args:
532 tag : Tag of TLV/DO to be written
533 data_hex : Hex string binary data to be written (value portion)
534 """
535 if not isinstance(self.selected_file, BerTlvEF):
536 raise TypeError("Only works with BER-TLV EF")
Harald Welte46255122023-10-21 23:40:42 +0200537 return self.scc.set_data(self.selected_file.fid, tag, data_hex, conserve=self.rs.conserve_write)
Harald Welte531894d2023-07-11 19:11:11 +0200538
539 def unregister_cmds(self, cmd_app=None):
540 """Unregister all file specific commands."""
541 if cmd_app and self.selected_file.shell_commands:
542 for c in self.selected_file.shell_commands:
543 cmd_app.unregister_command_set(c)
544
545
546