blob: 24472226ccabca4ae15ab5e1b6a594a203fe84f6 [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:
113 data, sw = self.card.select_adf_by_aid(f.aid)
Philipp Maier578cf122023-10-25 18:02:41 +0200114 self.selected_adf = f
Harald Welte531894d2023-07-11 19:11:11 +0200115 if sw == "9000":
116 print(" %s: %s" % (f.name, f.aid))
117 apps_taken.append(f)
118 except (SwMatchError, ProtocolError):
119 pass
120 return apps_taken
121
122 def reset(self, cmd_app=None) -> Hexstr:
123 """Perform physical card reset and obtain ATR.
124 Args:
125 cmd_app : Command Application State (for unregistering old file commands)
126 """
127 # delete all lchan != 0 (basic lchan)
Philipp Maier92b93562023-07-21 11:38:26 +0200128 for lchan_nr in list(self.lchan.keys()):
Harald Welte531894d2023-07-11 19:11:11 +0200129 if lchan_nr == 0:
130 continue
131 del self.lchan[lchan_nr]
132 atr = i2h(self.card.reset())
133 # select MF to reset internal state and to verify card really works
134 self.lchan[0].select('MF', cmd_app)
135 self.lchan[0].selected_adf = None
136 return atr
137
138 def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan':
139 """Add a logical channel to the runtime state. You shouldn't call this
140 directly but always go through RuntimeLchan.add_lchan()."""
141 if lchan_nr in self.lchan.keys():
142 raise ValueError('Cannot create already-existing lchan %d' % lchan_nr)
143 self.lchan[lchan_nr] = RuntimeLchan(lchan_nr, self)
144 return self.lchan[lchan_nr]
145
146 def del_lchan(self, lchan_nr: int):
147 if lchan_nr in self.lchan.keys():
148 del self.lchan[lchan_nr]
149 return True
150 else:
151 return False
152
153 def get_lchan_by_cla(self, cla) -> Optional['RuntimeLchan']:
154 lchan_nr = lchan_nr_from_cla(cla)
155 if lchan_nr in self.lchan.keys():
156 return self.lchan[lchan_nr]
157 else:
158 return None
159
160
161class RuntimeLchan:
162 """Represent the runtime state of a logical channel with a card."""
163
164 def __init__(self, lchan_nr: int, rs: RuntimeState):
165 self.lchan_nr = lchan_nr
166 self.rs = rs
167 self.selected_file = self.rs.mf
168 self.selected_adf = None
169 self.selected_file_fcp = None
170 self.selected_file_fcp_hex = None
Harald Welte46255122023-10-21 23:40:42 +0200171 self.scc = self.rs.card._scc.fork_lchan(lchan_nr)
Harald Welte531894d2023-07-11 19:11:11 +0200172
173 def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan':
174 """Add a new logical channel from the current logical channel. Just affects
175 internal state, doesn't actually open a channel with the UICC."""
176 new_lchan = self.rs.add_lchan(lchan_nr)
177 # See TS 102 221 Table 8.3
178 if self.lchan_nr != 0:
179 new_lchan.selected_file = self.get_cwd()
180 new_lchan.selected_adf = self.selected_adf
181 return new_lchan
182
183 def selected_file_descriptor_byte(self) -> dict:
184 return self.selected_file_fcp['file_descriptor']['file_descriptor_byte']
185
186 def selected_file_shareable(self) -> bool:
187 return self.selected_file_descriptor_byte()['shareable']
188
189 def selected_file_structure(self) -> str:
190 return self.selected_file_descriptor_byte()['structure']
191
192 def selected_file_type(self) -> str:
193 return self.selected_file_descriptor_byte()['file_type']
194
195 def selected_file_num_of_rec(self) -> Optional[int]:
196 return self.selected_file_fcp['file_descriptor'].get('num_of_rec')
197
198 def get_cwd(self) -> CardDF:
199 """Obtain the current working directory.
200
201 Returns:
202 CardDF instance
203 """
204 if isinstance(self.selected_file, CardDF):
205 return self.selected_file
206 else:
207 return self.selected_file.parent
208
209 def get_application_df(self) -> Optional[CardADF]:
210 """Obtain the currently selected application DF (if any).
211
212 Returns:
213 CardADF() instance or None"""
214 # iterate upwards from selected file; check if any is an ADF
215 node = self.selected_file
216 while node.parent != node:
217 if isinstance(node, CardADF):
218 return node
219 node = node.parent
220 return None
221
222 def interpret_sw(self, sw: str):
223 """Interpret a given status word relative to the currently selected application
224 or the underlying card profile.
225
226 Args:
227 sw : Status word as string of 4 hex digits
228
229 Returns:
230 Tuple of two strings
231 """
232 res = None
233 adf = self.get_application_df()
234 if adf:
235 app = adf.application
236 # The application either comes with its own interpret_sw
237 # method or we will use the interpret_sw method from the
238 # card profile.
239 if app and hasattr(app, "interpret_sw"):
240 res = app.interpret_sw(sw)
241 return res or self.rs.profile.interpret_sw(sw)
242
243 def probe_file(self, fid: str, cmd_app=None):
244 """Blindly try to select a file and automatically add a matching file
Philipp Maier1da86362023-10-31 13:17:14 +0100245 object if the file actually exists."""
Harald Welte531894d2023-07-11 19:11:11 +0200246 if not is_hex(fid, 4, 4):
247 raise ValueError(
248 "Cannot select unknown file by name %s, only hexadecimal 4 digit FID is allowed" % fid)
249
250 try:
Harald Welte46255122023-10-21 23:40:42 +0200251 (data, sw) = self.scc.select_file(fid)
Harald Welte531894d2023-07-11 19:11:11 +0200252 except SwMatchError as swm:
253 k = self.interpret_sw(swm.sw_actual)
254 if not k:
255 raise(swm)
256 raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1]))
257
258 select_resp = self.selected_file.decode_select_response(data)
259 if (select_resp['file_descriptor']['file_descriptor_byte']['file_type'] == 'df'):
260 f = CardDF(fid=fid, sfid=None, name="DF." + str(fid).upper(),
261 desc="dedicated file, manually added at runtime")
262 else:
263 if (select_resp['file_descriptor']['file_descriptor_byte']['structure'] == 'transparent'):
264 f = TransparentEF(fid=fid, sfid=None, name="EF." + str(fid).upper(),
265 desc="elementary file, manually added at runtime")
266 else:
267 f = LinFixedEF(fid=fid, sfid=None, name="EF." + str(fid).upper(),
268 desc="elementary file, manually added at runtime")
269
270 self.selected_file.add_files([f])
271 self.selected_file = f
272 return select_resp, data
273
274 def _select_pre(self, cmd_app):
275 # unregister commands of old file
276 if cmd_app and self.selected_file.shell_commands:
277 for c in self.selected_file.shell_commands:
278 cmd_app.unregister_command_set(c)
279
280 def _select_post(self, cmd_app):
281 # register commands of new file
282 if cmd_app and self.selected_file.shell_commands:
283 for c in self.selected_file.shell_commands:
284 cmd_app.register_command_set(c)
285
286 def select_file(self, file: CardFile, cmd_app=None):
287 """Select a file (EF, DF, ADF, MF, ...).
288
289 Args:
290 file : CardFile [or derived class] instance
291 cmd_app : Command Application State (for unregistering old file commands)
292 """
293 # we need to find a path from our self.selected_file to the destination
294 inter_path = self.selected_file.build_select_path_to(file)
295 if not inter_path:
296 raise RuntimeError('Cannot determine path from %s to %s' % (self.selected_file, file))
297
298 self._select_pre(cmd_app)
299
300 for p in inter_path:
301 try:
302 if isinstance(p, CardADF):
Harald Welte6dd6f3e2023-10-22 10:28:18 +0200303 (data, sw) = self.rs.card.select_adf_by_aid(p.aid, scc=self.scc)
Harald Welte531894d2023-07-11 19:11:11 +0200304 self.selected_adf = p
305 else:
Harald Welte46255122023-10-21 23:40:42 +0200306 (data, sw) = self.scc.select_file(p.fid)
Harald Welte531894d2023-07-11 19:11:11 +0200307 self.selected_file = p
308 except SwMatchError as swm:
309 self._select_post(cmd_app)
310 raise(swm)
311
312 self._select_post(cmd_app)
313
314 def select(self, name: str, cmd_app=None):
315 """Select a file (EF, DF, ADF, MF, ...).
316
317 Args:
318 name : Name of file to select
319 cmd_app : Command Application State (for unregistering old file commands)
320 """
321 # handling of entire paths with multiple directories/elements
322 if '/' in name:
323 prev_sel_file = self.selected_file
324 pathlist = name.split('/')
325 # treat /DF.GSM/foo like MF/DF.GSM/foo
326 if pathlist[0] == '':
327 pathlist[0] = 'MF'
328 try:
329 for p in pathlist:
330 self.select(p, cmd_app)
331 return
332 except Exception as e:
333 # if any intermediate step fails, go back to where we were
334 self.select_file(prev_sel_file, cmd_app)
335 raise e
336
337 sels = self.selected_file.get_selectables()
338 if is_hex(name):
339 name = name.lower()
340
341 self._select_pre(cmd_app)
342
343 if name in sels:
344 f = sels[name]
345 try:
346 if isinstance(f, CardADF):
Harald Welte6dd6f3e2023-10-22 10:28:18 +0200347 (data, sw) = self.rs.card.select_adf_by_aid(f.aid, scc=self.scc)
Philipp Maier578cf122023-10-25 18:02:41 +0200348 self.selected_adf = f
Harald Welte531894d2023-07-11 19:11:11 +0200349 else:
Harald Welte46255122023-10-21 23:40:42 +0200350 (data, sw) = self.scc.select_file(f.fid)
Harald Welte531894d2023-07-11 19:11:11 +0200351 self.selected_file = f
352 except SwMatchError as swm:
353 k = self.interpret_sw(swm.sw_actual)
354 if not k:
355 raise(swm)
356 raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1]))
357 select_resp = f.decode_select_response(data)
358 else:
359 (select_resp, data) = self.probe_file(name, cmd_app)
360
361 # store the raw + decoded FCP for later reference
362 self.selected_file_fcp_hex = data
363 self.selected_file_fcp = select_resp
364
365 self._select_post(cmd_app)
366 return select_resp
367
368 def status(self):
369 """Request STATUS (current selected file FCP) from card."""
Harald Welte46255122023-10-21 23:40:42 +0200370 (data, sw) = self.scc.status()
Harald Welte531894d2023-07-11 19:11:11 +0200371 return self.selected_file.decode_select_response(data)
372
373 def get_file_for_selectable(self, name: str):
374 sels = self.selected_file.get_selectables()
375 return sels[name]
376
377 def activate_file(self, name: str):
378 """Request ACTIVATE FILE of specified file."""
379 sels = self.selected_file.get_selectables()
380 f = sels[name]
Harald Welte46255122023-10-21 23:40:42 +0200381 data, sw = self.scc.activate_file(f.fid)
Harald Welte531894d2023-07-11 19:11:11 +0200382 return data, sw
383
384 def read_binary(self, length: int = None, offset: int = 0):
385 """Read [part of] a transparent EF binary data.
386
387 Args:
388 length : Amount of data to read (None: as much as possible)
389 offset : Offset into the file from which to read 'length' bytes
390 Returns:
391 binary data read from the file
392 """
393 if not isinstance(self.selected_file, TransparentEF):
394 raise TypeError("Only works with TransparentEF")
Harald Welte46255122023-10-21 23:40:42 +0200395 return self.scc.read_binary(self.selected_file.fid, length, offset)
Harald Welte531894d2023-07-11 19:11:11 +0200396
397 def read_binary_dec(self) -> Tuple[dict, str]:
398 """Read [part of] a transparent EF binary data and decode it.
399
400 Args:
401 length : Amount of data to read (None: as much as possible)
402 offset : Offset into the file from which to read 'length' bytes
403 Returns:
404 abstract decode data read from the file
405 """
406 (data, sw) = self.read_binary()
407 dec_data = self.selected_file.decode_hex(data)
408 return (dec_data, sw)
409
410 def update_binary(self, data_hex: str, offset: int = 0):
411 """Update transparent EF binary data.
412
413 Args:
414 data_hex : hex string of data to be written
415 offset : Offset into the file from which to write 'data_hex'
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.update_binary(self.selected_file.fid, data_hex, offset, conserve=self.rs.conserve_write)
Harald Welte531894d2023-07-11 19:11:11 +0200420
421 def update_binary_dec(self, data: dict):
422 """Update transparent EF from abstract data. Encodes the data to binary and
423 then updates the EF with it.
424
425 Args:
426 data : abstract data which is to be encoded and written
427 """
428 data_hex = self.selected_file.encode_hex(data)
429 return self.update_binary(data_hex)
430
431 def read_record(self, rec_nr: int = 0):
432 """Read a record as binary data.
433
434 Args:
435 rec_nr : Record number to read
436 Returns:
437 hex string of binary data contained in record
438 """
439 if not isinstance(self.selected_file, LinFixedEF):
440 raise TypeError("Only works with Linear Fixed EF")
441 # returns a string of hex nibbles
Harald Welte46255122023-10-21 23:40:42 +0200442 return self.scc.read_record(self.selected_file.fid, rec_nr)
Harald Welte531894d2023-07-11 19:11:11 +0200443
444 def read_record_dec(self, rec_nr: int = 0) -> Tuple[dict, str]:
445 """Read a record and decode it to abstract data.
446
447 Args:
448 rec_nr : Record number to read
449 Returns:
450 abstract data contained in record
451 """
452 (data, sw) = self.read_record(rec_nr)
453 return (self.selected_file.decode_record_hex(data, rec_nr), sw)
454
455 def update_record(self, rec_nr: int, data_hex: str):
456 """Update a record with given binary data
457
458 Args:
459 rec_nr : Record number to read
460 data_hex : Hex string binary data to be written
461 """
462 if not isinstance(self.selected_file, LinFixedEF):
463 raise TypeError("Only works with Linear Fixed EF")
Harald Welte46255122023-10-21 23:40:42 +0200464 return self.scc.update_record(self.selected_file.fid, rec_nr, data_hex,
Philipp Maier37e57e02023-09-07 12:43:12 +0200465 conserve=self.rs.conserve_write,
466 leftpad=self.selected_file.leftpad)
Harald Welte531894d2023-07-11 19:11:11 +0200467
468 def update_record_dec(self, rec_nr: int, data: dict):
469 """Update a record with given abstract data. Will encode abstract to binary data
470 and then write it to the given record on the card.
471
472 Args:
473 rec_nr : Record number to read
474 data_hex : Abstract data to be written
475 """
476 data_hex = self.selected_file.encode_record_hex(data, rec_nr)
477 return self.update_record(rec_nr, data_hex)
478
479 def retrieve_data(self, tag: int = 0):
480 """Read a DO/TLV as binary data.
481
482 Args:
483 tag : Tag of TLV/DO to read
484 Returns:
485 hex string of full BER-TLV DO including Tag and Length
486 """
487 if not isinstance(self.selected_file, BerTlvEF):
488 raise TypeError("Only works with BER-TLV EF")
489 # returns a string of hex nibbles
Harald Welte46255122023-10-21 23:40:42 +0200490 return self.scc.retrieve_data(self.selected_file.fid, tag)
Harald Welte531894d2023-07-11 19:11:11 +0200491
492 def retrieve_tags(self):
493 """Retrieve tags available on BER-TLV EF.
494
495 Returns:
496 list of integer tags contained in EF
497 """
498 if not isinstance(self.selected_file, BerTlvEF):
499 raise TypeError("Only works with BER-TLV EF")
Harald Welte46255122023-10-21 23:40:42 +0200500 data, sw = self.scc.retrieve_data(self.selected_file.fid, 0x5c)
Harald Welte531894d2023-07-11 19:11:11 +0200501 tag, length, value, remainder = bertlv_parse_one(h2b(data))
502 return list(value)
503
504 def set_data(self, tag: int, data_hex: str):
505 """Update a TLV/DO with given binary data
506
507 Args:
508 tag : Tag of TLV/DO to be written
509 data_hex : Hex string binary data to be written (value portion)
510 """
511 if not isinstance(self.selected_file, BerTlvEF):
512 raise TypeError("Only works with BER-TLV EF")
Harald Welte46255122023-10-21 23:40:42 +0200513 return self.scc.set_data(self.selected_file.fid, tag, data_hex, conserve=self.rs.conserve_write)
Harald Welte531894d2023-07-11 19:11:11 +0200514
515 def unregister_cmds(self, cmd_app=None):
516 """Unregister all file specific commands."""
517 if cmd_app and self.selected_file.shell_commands:
518 for c in self.selected_file.shell_commands:
519 cmd_app.unregister_command_set(c)
520
521
522