blob: 04e849babca88e831af1d9947ac2ef113e63e460 [file] [log] [blame]
Harald Welteb2edd142021-01-08 23:29:35 +01001# coding=utf-8
2"""Representation of the ISO7816-4 filesystem model.
3
4The File (and its derived classes) represent the structure / hierarchy
5of the ISO7816-4 smart card file system with the MF, DF, EF and ADF
6entries, further sub-divided into the EF sub-types Transparent, Linear Fixed, etc.
7
8The classes are intended to represent the *specification* of the filesystem,
9not the actual contents / runtime state of interacting with a given smart card.
Harald Welteb2edd142021-01-08 23:29:35 +010010"""
11
Harald Welte5a4fd522021-04-02 16:05:26 +020012# (C) 2021 by Harald Welte <laforge@osmocom.org>
13#
14# This program is free software: you can redistribute it and/or modify
15# it under the terms of the GNU General Public License as published by
16# the Free Software Foundation, either version 2 of the License, or
17# (at your option) any later version.
18#
19# This program is distributed in the hope that it will be useful,
20# but WITHOUT ANY WARRANTY; without even the implied warranty of
21# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22# GNU General Public License for more details.
23#
24# You should have received a copy of the GNU General Public License
25# along with this program. If not, see <http://www.gnu.org/licenses/>.
26
Harald Welteb2edd142021-01-08 23:29:35 +010027import code
Harald Welte4145d3c2021-04-08 20:34:13 +020028import tempfile
Harald Welteb2edd142021-01-08 23:29:35 +010029import json
Harald Weltef44256c2021-10-14 15:53:39 +020030import abc
31import inspect
Harald Welteb2edd142021-01-08 23:29:35 +010032
33import cmd2
34from cmd2 import CommandSet, with_default_category, with_argparser
35import argparse
36
Harald Welte9170fbf2022-02-11 21:54:37 +010037from typing import cast, Optional, Iterable, List, Dict, Tuple, Union
Harald Welteee3501f2021-04-02 13:00:18 +020038
Harald Weltef44256c2021-10-14 15:53:39 +020039from smartcard.util import toBytes
40
Harald Weltedaf2b392021-05-03 23:17:29 +020041from pySim.utils import sw_match, h2b, b2h, i2h, is_hex, auto_int, bertlv_parse_one, Hexstr
Harald Welte07c7b1f2021-05-28 22:01:29 +020042from pySim.construct import filter_dict, parse_construct
Harald Welteb2edd142021-01-08 23:29:35 +010043from pySim.exceptions import *
Harald Welte0d4e98a2021-04-07 00:14:40 +020044from pySim.jsonpath import js_path_find, js_path_modify
Harald Weltef44256c2021-10-14 15:53:39 +020045from pySim.commands import SimCardCommands
Harald Welteb2edd142021-01-08 23:29:35 +010046
Harald Welte9170fbf2022-02-11 21:54:37 +010047# int: a single service is associated with this file
48# list: any of the listed services requires this file
49# tuple: logical-and of the listed services requires this file
50CardFileService = Union[int, List[int], Tuple[int, ...]]
Harald Weltec91085e2022-02-10 18:05:45 +010051
Harald Welte13edf302022-07-21 15:19:23 +020052Size = Tuple[int, Optional[int]]
53
Harald Weltea6c0f882022-07-17 14:23:17 +020054def lchan_nr_from_cla(cla: int) -> int:
55 """Resolve the logical channel number from the CLA byte."""
56 # TS 102 221 10.1.1 Coding of Class Byte
57 if cla >> 4 in [0x0, 0xA, 0x8]:
58 # Table 10.3
59 return cla & 0x03
60 elif cla & 0xD0 in [0x40, 0xC0]:
61 # Table 10.4a
62 return 4 + (cla & 0x0F)
63 else:
64 raise ValueError('Could not determine logical channel for CLA=%2X' % cla)
65
Vadim Yanitskiy04b5d9d2022-07-07 03:05:30 +070066class CardFile:
Harald Welteb2edd142021-01-08 23:29:35 +010067 """Base class for all objects in the smart card filesystem.
68 Serve as a common ancestor to all other file types; rarely used directly.
69 """
70 RESERVED_NAMES = ['..', '.', '/', 'MF']
71 RESERVED_FIDS = ['3f00']
72
Harald Weltec91085e2022-02-10 18:05:45 +010073 def __init__(self, fid: str = None, sfid: str = None, name: str = None, desc: str = None,
Harald Welte9170fbf2022-02-11 21:54:37 +010074 parent: Optional['CardDF'] = None, profile: Optional['CardProfile'] = None,
75 service: Optional[CardFileService] = None):
Harald Welteee3501f2021-04-02 13:00:18 +020076 """
77 Args:
78 fid : File Identifier (4 hex digits)
79 sfid : Short File Identifier (2 hex digits, optional)
80 name : Brief name of the file, lik EF_ICCID
Harald Weltec9cdce32021-04-11 10:28:28 +020081 desc : Description of the file
Harald Welteee3501f2021-04-02 13:00:18 +020082 parent : Parent CardFile object within filesystem hierarchy
Philipp Maier5af7bdf2021-11-04 12:48:41 +010083 profile : Card profile that this file should be part of
Harald Welte9170fbf2022-02-11 21:54:37 +010084 service : Service (SST/UST/IST) associated with the file
Harald Welteee3501f2021-04-02 13:00:18 +020085 """
Harald Welteb2edd142021-01-08 23:29:35 +010086 if not isinstance(self, CardADF) and fid == None:
87 raise ValueError("fid is mandatory")
88 if fid:
89 fid = fid.lower()
90 self.fid = fid # file identifier
91 self.sfid = sfid # short file identifier
92 self.name = name # human readable name
93 self.desc = desc # human readable description
94 self.parent = parent
95 if self.parent and self.parent != self and self.fid:
96 self.parent.add_file(self)
Philipp Maier5af7bdf2021-11-04 12:48:41 +010097 self.profile = profile
Harald Welte9170fbf2022-02-11 21:54:37 +010098 self.service = service
Harald Weltec91085e2022-02-10 18:05:45 +010099 self.shell_commands = [] # type: List[CommandSet]
Harald Welteb2edd142021-01-08 23:29:35 +0100100
Harald Weltec91085e2022-02-10 18:05:45 +0100101 # Note: the basic properties (fid, name, ect.) are verified when
102 # the file is attached to a parent file. See method add_file() in
103 # class Card DF
Philipp Maier66061582021-03-09 21:57:57 +0100104
Harald Welteb2edd142021-01-08 23:29:35 +0100105 def __str__(self):
106 if self.name:
107 return self.name
108 else:
109 return self.fid
110
Harald Weltec91085e2022-02-10 18:05:45 +0100111 def _path_element(self, prefer_name: bool) -> Optional[str]:
Harald Welteb2edd142021-01-08 23:29:35 +0100112 if prefer_name and self.name:
113 return self.name
114 else:
115 return self.fid
116
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200117 def fully_qualified_path_str(self, prefer_name: bool = True) -> str:
118 """Return fully qualified path to file as string.
119
120 Args:
121 prefer_name : Preferably build path of names; fall-back to FIDs as required
122 """
123 return '/'.join(self.fully_qualified_path(prefer_name))
124
Harald Weltec91085e2022-02-10 18:05:45 +0100125 def fully_qualified_path(self, prefer_name: bool = True) -> List[str]:
Harald Welteee3501f2021-04-02 13:00:18 +0200126 """Return fully qualified path to file as list of FID or name strings.
127
128 Args:
129 prefer_name : Preferably build path of names; fall-back to FIDs as required
130 """
Harald Welte1e456572021-04-02 17:16:30 +0200131 if self.parent and self.parent != self:
Harald Welteb2edd142021-01-08 23:29:35 +0100132 ret = self.parent.fully_qualified_path(prefer_name)
133 else:
134 ret = []
Harald Welte1e456572021-04-02 17:16:30 +0200135 elem = self._path_element(prefer_name)
136 if elem:
137 ret.append(elem)
Harald Welteb2edd142021-01-08 23:29:35 +0100138 return ret
139
Harald Welteaceb2a52022-02-12 21:41:59 +0100140 def fully_qualified_path_fobj(self) -> List['CardFile']:
141 """Return fully qualified path to file as list of CardFile instance references."""
142 if self.parent and self.parent != self:
143 ret = self.parent.fully_qualified_path_fobj()
144 else:
145 ret = []
146 if self:
147 ret.append(self)
148 return ret
149
150 def build_select_path_to(self, target: 'CardFile') -> Optional[List['CardFile']]:
151 """Build the relative sequence of files we need to traverse to get from us to 'target'."""
152 cur_fqpath = self.fully_qualified_path_fobj()
153 target_fqpath = target.fully_qualified_path_fobj()
154 inter_path = []
155 cur_fqpath.pop() # drop last element (currently selected file, doesn't need re-selection
156 cur_fqpath.reverse()
157 for ce in cur_fqpath:
158 inter_path.append(ce)
159 for i in range(0, len(target_fqpath)-1):
160 te = target_fqpath[i]
161 if te == ce:
162 for te2 in target_fqpath[i+1:]:
163 inter_path.append(te2)
164 # we found our common ancestor
165 return inter_path
166 return None
167
Harald Welteee3501f2021-04-02 13:00:18 +0200168 def get_mf(self) -> Optional['CardMF']:
Harald Welteb2edd142021-01-08 23:29:35 +0100169 """Return the MF (root) of the file system."""
170 if self.parent == None:
171 return None
172 # iterate towards the top. MF has parent == self
173 node = self
Harald Welte1e456572021-04-02 17:16:30 +0200174 while node.parent and node.parent != node:
Harald Welteb2edd142021-01-08 23:29:35 +0100175 node = node.parent
Harald Welte1e456572021-04-02 17:16:30 +0200176 return cast(CardMF, node)
Harald Welteb2edd142021-01-08 23:29:35 +0100177
Harald Weltec91085e2022-02-10 18:05:45 +0100178 def _get_self_selectables(self, alias: str = None, flags=[]) -> Dict[str, 'CardFile']:
Harald Welteee3501f2021-04-02 13:00:18 +0200179 """Return a dict of {'identifier': self} tuples.
180
181 Args:
182 alias : Add an alias with given name to 'self'
183 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
184 If not specified, all selectables will be returned.
185 Returns:
186 dict containing reference to 'self' for all identifiers.
187 """
Harald Welteb2edd142021-01-08 23:29:35 +0100188 sels = {}
189 if alias:
190 sels.update({alias: self})
Philipp Maier786f7812021-02-25 16:48:10 +0100191 if self.fid and (flags == [] or 'FIDS' in flags):
Harald Welteb2edd142021-01-08 23:29:35 +0100192 sels.update({self.fid: self})
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100193 if self.name and (flags == [] or 'FNAMES' in flags):
Harald Welteb2edd142021-01-08 23:29:35 +0100194 sels.update({self.name: self})
195 return sels
196
Harald Weltef5ff1b82022-07-24 12:19:57 +0200197 def _get_parent_selectables(self, alias: Optional[str] = None, flags=[]) -> Dict[str, 'CardFile']:
198 sels = {}
199 if not self.parent or self.parent == self:
200 return sels
201 # add our immediate parent
202 if alias:
203 sels.update({alias: self.parent})
204 if self.parent.fid and (flags == [] or 'FIDS' in flags):
205 sels.update({self.parent.fid: self.parent})
206 if self.parent.name and (flags == [] or 'FNAMES' in flags):
207 sels.update({self.parent.name: self.parent})
208 # recurse to parents of our parent, but without any alias
209 sels.update(self.parent._get_parent_selectables(None, flags))
210 return sels
211
Harald Weltec91085e2022-02-10 18:05:45 +0100212 def get_selectables(self, flags=[]) -> Dict[str, 'CardFile']:
Harald Welteee3501f2021-04-02 13:00:18 +0200213 """Return a dict of {'identifier': File} that is selectable from the current file.
214
215 Args:
216 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
217 If not specified, all selectables will be returned.
218 Returns:
219 dict containing all selectable items. Key is identifier (string), value
220 a reference to a CardFile (or derived class) instance.
221 """
Philipp Maier786f7812021-02-25 16:48:10 +0100222 sels = {}
Harald Welteb2edd142021-01-08 23:29:35 +0100223 # we can always select ourself
Philipp Maier786f7812021-02-25 16:48:10 +0100224 if flags == [] or 'SELF' in flags:
225 sels = self._get_self_selectables('.', flags)
Harald Welteb2edd142021-01-08 23:29:35 +0100226 # we can always select our parent
Philipp Maier786f7812021-02-25 16:48:10 +0100227 if flags == [] or 'PARENT' in flags:
Harald Weltef5ff1b82022-07-24 12:19:57 +0200228 sels.update(self._get_parent_selectables('..', flags))
Harald Welteb2edd142021-01-08 23:29:35 +0100229 # if we have a MF, we can always select its applications
Philipp Maier786f7812021-02-25 16:48:10 +0100230 if flags == [] or 'MF' in flags:
231 mf = self.get_mf()
232 if mf:
Harald Weltec91085e2022-02-10 18:05:45 +0100233 sels.update(mf._get_self_selectables(flags=flags))
234 sels.update(mf.get_app_selectables(flags=flags))
Harald Welteb2edd142021-01-08 23:29:35 +0100235 return sels
236
Harald Weltec91085e2022-02-10 18:05:45 +0100237 def get_selectable_names(self, flags=[]) -> List[str]:
Harald Welteee3501f2021-04-02 13:00:18 +0200238 """Return a dict of {'identifier': File} that is selectable from the current file.
239
240 Args:
241 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
242 If not specified, all selectables will be returned.
243 Returns:
Harald Welte1e456572021-04-02 17:16:30 +0200244 list containing all selectable names.
Harald Welteee3501f2021-04-02 13:00:18 +0200245 """
Philipp Maier786f7812021-02-25 16:48:10 +0100246 sels = self.get_selectables(flags)
Harald Welteb3d68c02022-01-21 15:31:29 +0100247 sel_keys = list(sels.keys())
248 sel_keys.sort()
249 return sel_keys
Harald Welteb2edd142021-01-08 23:29:35 +0100250
Harald Weltec91085e2022-02-10 18:05:45 +0100251 def decode_select_response(self, data_hex: str):
Philipp Maier5af7bdf2021-11-04 12:48:41 +0100252 """Decode the response to a SELECT command.
253
254 Args:
Harald Weltec91085e2022-02-10 18:05:45 +0100255 data_hex: Hex string of the select response
256 """
Philipp Maier5af7bdf2021-11-04 12:48:41 +0100257
Harald Weltec91085e2022-02-10 18:05:45 +0100258 # When the current file does not implement a custom select response decoder,
259 # we just ask the parent file to decode the select response. If this method
260 # is not overloaded by the current file we will again ask the parent file.
261 # This way we recursively travel up the file system tree until we hit a file
262 # that does implement a concrete decoder.
Harald Welte1e456572021-04-02 17:16:30 +0200263 if self.parent:
264 return self.parent.decode_select_response(data_hex)
Harald Welteb2edd142021-01-08 23:29:35 +0100265
Philipp Maier5af7bdf2021-11-04 12:48:41 +0100266 def get_profile(self):
267 """Get the profile associated with this file. If this file does not have any
268 profile assigned, try to find a file above (usually the MF) in the filesystem
269 hirarchy that has a profile assigned
270 """
271
272 # If we have a profile set, return it
273 if self.profile:
274 return self.profile
275
276 # Walk up recursively until we hit a parent that has a profile set
277 if self.parent:
278 return self.parent.get_profile()
279 return None
Harald Welteb2edd142021-01-08 23:29:35 +0100280
Harald Welte9170fbf2022-02-11 21:54:37 +0100281 def should_exist_for_services(self, services: List[int]):
282 """Assuming the provided list of activated services, should this file exist and be activated?."""
283 if self.service is None:
284 return None
285 elif isinstance(self.service, int):
286 # a single service determines the result
287 return self.service in services
288 elif isinstance(self.service, list):
289 # any of the services active -> true
290 for s in self.service:
291 if s in services:
292 return True
293 return False
294 elif isinstance(self.service, tuple):
295 # all of the services active -> true
296 for s in self.service:
297 if not s in services:
298 return False
299 return True
300 else:
301 raise ValueError("self.service must be either int or list or tuple")
302
Harald Weltec91085e2022-02-10 18:05:45 +0100303
Harald Welteb2edd142021-01-08 23:29:35 +0100304class CardDF(CardFile):
305 """DF (Dedicated File) in the smart card filesystem. Those are basically sub-directories."""
Philipp Maier63f572d2021-03-09 22:42:47 +0100306
307 @with_default_category('DF/ADF Commands')
308 class ShellCommands(CommandSet):
309 def __init__(self):
310 super().__init__()
311
Harald Welteb2edd142021-01-08 23:29:35 +0100312 def __init__(self, **kwargs):
313 if not isinstance(self, CardADF):
314 if not 'fid' in kwargs:
315 raise TypeError('fid is mandatory for all DF')
316 super().__init__(**kwargs)
317 self.children = dict()
Philipp Maier63f572d2021-03-09 22:42:47 +0100318 self.shell_commands = [self.ShellCommands()]
Harald Welte9170fbf2022-02-11 21:54:37 +0100319 # dict of CardFile affected by service(int), indexed by service
320 self.files_by_service = {}
Harald Welteb2edd142021-01-08 23:29:35 +0100321
322 def __str__(self):
323 return "DF(%s)" % (super().__str__())
324
Harald Welte9170fbf2022-02-11 21:54:37 +0100325 def _add_file_services(self, child):
326 """Add a child (DF/EF) to the files_by_services of the parent."""
327 if not child.service:
328 return
329 if isinstance(child.service, int):
330 self.files_by_service.setdefault(child.service, []).append(child)
331 elif isinstance(child.service, list):
332 for service in child.service:
333 self.files_by_service.setdefault(service, []).append(child)
334 elif isinstance(child.service, tuple):
335 for service in child.service:
336 self.files_by_service.setdefault(service, []).append(child)
337 else:
338 raise ValueError
339
Harald Welted56f45d2022-07-16 11:46:59 +0200340 def _has_service(self):
341 if self.service:
342 return True
343 for c in self.children.values():
344 if isinstance(c, CardDF):
345 if c._has_service():
346 return True
347
Harald Weltec91085e2022-02-10 18:05:45 +0100348 def add_file(self, child: CardFile, ignore_existing: bool = False):
Harald Welteee3501f2021-04-02 13:00:18 +0200349 """Add a child (DF/EF) to this DF.
350 Args:
351 child: The new DF/EF to be added
352 ignore_existing: Ignore, if file with given FID already exists. Old one will be kept.
353 """
Harald Welteb2edd142021-01-08 23:29:35 +0100354 if not isinstance(child, CardFile):
355 raise TypeError("Expected a File instance")
Harald Weltec91085e2022-02-10 18:05:45 +0100356 if not is_hex(child.fid, minlen=4, maxlen=4):
Philipp Maier3aec8712021-03-09 21:49:01 +0100357 raise ValueError("File name %s is not a valid fid" % (child.fid))
Harald Welteb2edd142021-01-08 23:29:35 +0100358 if child.name in CardFile.RESERVED_NAMES:
359 raise ValueError("File name %s is a reserved name" % (child.name))
360 if child.fid in CardFile.RESERVED_FIDS:
Philipp Maiere8bc1b42021-03-09 20:33:41 +0100361 raise ValueError("File fid %s is a reserved fid" % (child.fid))
Harald Welteb2edd142021-01-08 23:29:35 +0100362 if child.fid in self.children:
363 if ignore_existing:
364 return
Harald Weltec91085e2022-02-10 18:05:45 +0100365 raise ValueError(
366 "File with given fid %s already exists in %s" % (child.fid, self))
Harald Welteb2edd142021-01-08 23:29:35 +0100367 if self.lookup_file_by_sfid(child.sfid):
Harald Weltec91085e2022-02-10 18:05:45 +0100368 raise ValueError(
369 "File with given sfid %s already exists in %s" % (child.sfid, self))
Harald Welteb2edd142021-01-08 23:29:35 +0100370 if self.lookup_file_by_name(child.name):
371 if ignore_existing:
372 return
Harald Weltec91085e2022-02-10 18:05:45 +0100373 raise ValueError(
374 "File with given name %s already exists in %s" % (child.name, self))
Harald Welteb2edd142021-01-08 23:29:35 +0100375 self.children[child.fid] = child
376 child.parent = self
Harald Welte419bb492022-02-12 21:39:35 +0100377 # update the service -> file relationship table
Harald Welte9170fbf2022-02-11 21:54:37 +0100378 self._add_file_services(child)
Harald Welte419bb492022-02-12 21:39:35 +0100379 if isinstance(child, CardDF):
380 for c in child.children.values():
381 self._add_file_services(c)
382 if isinstance(c, CardDF):
Harald Welted56f45d2022-07-16 11:46:59 +0200383 for gc in c.children.values():
384 if isinstance(gc, CardDF):
385 if gc._has_service():
386 raise ValueError('TODO: implement recursive service -> file mapping')
Harald Welteb2edd142021-01-08 23:29:35 +0100387
Harald Weltec91085e2022-02-10 18:05:45 +0100388 def add_files(self, children: Iterable[CardFile], ignore_existing: bool = False):
Harald Welteee3501f2021-04-02 13:00:18 +0200389 """Add a list of child (DF/EF) to this DF
390
391 Args:
392 children: List of new DF/EFs to be added
393 ignore_existing: Ignore, if file[s] with given FID already exists. Old one[s] will be kept.
394 """
Harald Welteb2edd142021-01-08 23:29:35 +0100395 for child in children:
396 self.add_file(child, ignore_existing)
397
Harald Weltec91085e2022-02-10 18:05:45 +0100398 def get_selectables(self, flags=[]) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +0200399 """Return a dict of {'identifier': File} that is selectable from the current DF.
400
401 Args:
402 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
403 If not specified, all selectables will be returned.
404 Returns:
405 dict containing all selectable items. Key is identifier (string), value
406 a reference to a CardFile (or derived class) instance.
407 """
Harald Welteb2edd142021-01-08 23:29:35 +0100408 # global selectables + our children
Philipp Maier786f7812021-02-25 16:48:10 +0100409 sels = super().get_selectables(flags)
410 if flags == [] or 'FIDS' in flags:
Harald Weltec91085e2022-02-10 18:05:45 +0100411 sels.update({x.fid: x for x in self.children.values() if x.fid})
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100412 if flags == [] or 'FNAMES' in flags:
Harald Weltec91085e2022-02-10 18:05:45 +0100413 sels.update({x.name: x for x in self.children.values() if x.name})
Harald Welteb2edd142021-01-08 23:29:35 +0100414 return sels
415
Harald Weltec91085e2022-02-10 18:05:45 +0100416 def lookup_file_by_name(self, name: Optional[str]) -> Optional[CardFile]:
Harald Welteee3501f2021-04-02 13:00:18 +0200417 """Find a file with given name within current DF."""
Harald Welteb2edd142021-01-08 23:29:35 +0100418 if name == None:
419 return None
420 for i in self.children.values():
421 if i.name and i.name == name:
422 return i
423 return None
424
Harald Weltec91085e2022-02-10 18:05:45 +0100425 def lookup_file_by_sfid(self, sfid: Optional[str]) -> Optional[CardFile]:
Harald Welteee3501f2021-04-02 13:00:18 +0200426 """Find a file with given short file ID within current DF."""
Harald Welteb2edd142021-01-08 23:29:35 +0100427 if sfid == None:
428 return None
429 for i in self.children.values():
Harald Welte1e456572021-04-02 17:16:30 +0200430 if i.sfid == int(str(sfid)):
Harald Welteb2edd142021-01-08 23:29:35 +0100431 return i
432 return None
433
Harald Weltec91085e2022-02-10 18:05:45 +0100434 def lookup_file_by_fid(self, fid: str) -> Optional[CardFile]:
Harald Welteee3501f2021-04-02 13:00:18 +0200435 """Find a file with given file ID within current DF."""
Harald Welteb2edd142021-01-08 23:29:35 +0100436 if fid in self.children:
437 return self.children[fid]
438 return None
439
440
441class CardMF(CardDF):
442 """MF (Master File) in the smart card filesystem"""
Harald Weltec91085e2022-02-10 18:05:45 +0100443
Harald Welteb2edd142021-01-08 23:29:35 +0100444 def __init__(self, **kwargs):
445 # can be overridden; use setdefault
446 kwargs.setdefault('fid', '3f00')
447 kwargs.setdefault('name', 'MF')
448 kwargs.setdefault('desc', 'Master File (directory root)')
449 # cannot be overridden; use assignment
450 kwargs['parent'] = self
451 super().__init__(**kwargs)
452 self.applications = dict()
453
454 def __str__(self):
455 return "MF(%s)" % (self.fid)
456
Harald Weltec91085e2022-02-10 18:05:45 +0100457 def add_application_df(self, app: 'CardADF'):
Harald Welte5ce35242021-04-02 20:27:05 +0200458 """Add an Application to the MF"""
Harald Welteb2edd142021-01-08 23:29:35 +0100459 if not isinstance(app, CardADF):
460 raise TypeError("Expected an ADF instance")
461 if app.aid in self.applications:
462 raise ValueError("AID %s already exists" % (app.aid))
463 self.applications[app.aid] = app
Harald Weltec91085e2022-02-10 18:05:45 +0100464 app.parent = self
Harald Welteb2edd142021-01-08 23:29:35 +0100465
466 def get_app_names(self):
467 """Get list of completions (AID names)"""
Harald Welted53918c2022-02-12 18:20:49 +0100468 return list(self.applications.values())
Harald Welteb2edd142021-01-08 23:29:35 +0100469
Harald Weltec91085e2022-02-10 18:05:45 +0100470 def get_selectables(self, flags=[]) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +0200471 """Return a dict of {'identifier': File} that is selectable from the current DF.
472
473 Args:
474 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
475 If not specified, all selectables will be returned.
476 Returns:
477 dict containing all selectable items. Key is identifier (string), value
478 a reference to a CardFile (or derived class) instance.
479 """
Philipp Maier786f7812021-02-25 16:48:10 +0100480 sels = super().get_selectables(flags)
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100481 sels.update(self.get_app_selectables(flags))
Harald Welteb2edd142021-01-08 23:29:35 +0100482 return sels
483
Harald Weltec91085e2022-02-10 18:05:45 +0100484 def get_app_selectables(self, flags=[]) -> dict:
Philipp Maier786f7812021-02-25 16:48:10 +0100485 """Get applications by AID + name"""
486 sels = {}
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100487 if flags == [] or 'AIDS' in flags:
Harald Weltec91085e2022-02-10 18:05:45 +0100488 sels.update({x.aid: x for x in self.applications.values()})
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100489 if flags == [] or 'ANAMES' in flags:
Harald Weltec91085e2022-02-10 18:05:45 +0100490 sels.update(
491 {x.name: x for x in self.applications.values() if x.name})
Harald Welteb2edd142021-01-08 23:29:35 +0100492 return sels
493
Harald Weltec9752512022-02-11 16:31:15 +0100494 def decode_select_response(self, data_hex: Optional[str]) -> object:
Harald Welteee3501f2021-04-02 13:00:18 +0200495 """Decode the response to a SELECT command.
496
Philipp Maier5af7bdf2021-11-04 12:48:41 +0100497 This is the fall-back method which automatically defers to the standard decoding
498 method defined by the card profile. When no profile is set, then no decoding is
Harald Weltec91085e2022-02-10 18:05:45 +0100499 performed. Specific derived classes (usually ADF) can overload this method to
500 install specific decoding.
Harald Welteee3501f2021-04-02 13:00:18 +0200501 """
Harald Welteb2edd142021-01-08 23:29:35 +0100502
Harald Weltec9752512022-02-11 16:31:15 +0100503 if not data_hex:
504 return data_hex
505
Philipp Maier5af7bdf2021-11-04 12:48:41 +0100506 profile = self.get_profile()
Harald Welteb2edd142021-01-08 23:29:35 +0100507
Philipp Maier5af7bdf2021-11-04 12:48:41 +0100508 if profile:
509 return profile.decode_select_response(data_hex)
510 else:
511 return data_hex
Harald Welteb2edd142021-01-08 23:29:35 +0100512
Harald Weltec91085e2022-02-10 18:05:45 +0100513
Harald Welteb2edd142021-01-08 23:29:35 +0100514class CardADF(CardDF):
515 """ADF (Application Dedicated File) in the smart card filesystem"""
Harald Weltec91085e2022-02-10 18:05:45 +0100516
517 def __init__(self, aid: str, **kwargs):
Harald Welteb2edd142021-01-08 23:29:35 +0100518 super().__init__(**kwargs)
Harald Welte5ce35242021-04-02 20:27:05 +0200519 # reference to CardApplication may be set from CardApplication constructor
Harald Weltefe8a7442021-04-10 11:51:54 +0200520 self.application = None # type: Optional[CardApplication]
Harald Welteb2edd142021-01-08 23:29:35 +0100521 self.aid = aid # Application Identifier
Harald Welte1e456572021-04-02 17:16:30 +0200522 mf = self.get_mf()
523 if mf:
Harald Welte5ce35242021-04-02 20:27:05 +0200524 mf.add_application_df(self)
Harald Welteb2edd142021-01-08 23:29:35 +0100525
526 def __str__(self):
Harald Welte4b003652022-07-16 11:55:07 +0200527 return "ADF(%s)" % (self.name if self.name else self.aid)
Harald Welteb2edd142021-01-08 23:29:35 +0100528
Harald Weltec91085e2022-02-10 18:05:45 +0100529 def _path_element(self, prefer_name: bool):
Harald Welteb2edd142021-01-08 23:29:35 +0100530 if self.name and prefer_name:
531 return self.name
532 else:
533 return self.aid
534
535
536class CardEF(CardFile):
537 """EF (Entry File) in the smart card filesystem"""
Harald Weltec91085e2022-02-10 18:05:45 +0100538
Harald Welteb2edd142021-01-08 23:29:35 +0100539 def __init__(self, *, fid, **kwargs):
540 kwargs['fid'] = fid
541 super().__init__(**kwargs)
542
543 def __str__(self):
544 return "EF(%s)" % (super().__str__())
545
Harald Weltec91085e2022-02-10 18:05:45 +0100546 def get_selectables(self, flags=[]) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +0200547 """Return a dict of {'identifier': File} that is selectable from the current DF.
548
549 Args:
550 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
551 If not specified, all selectables will be returned.
552 Returns:
553 dict containing all selectable items. Key is identifier (string), value
554 a reference to a CardFile (or derived class) instance.
555 """
Harald Weltec91085e2022-02-10 18:05:45 +0100556 # global selectable names + those of the parent DF
Philipp Maier786f7812021-02-25 16:48:10 +0100557 sels = super().get_selectables(flags)
Harald Welted2c177b2022-07-24 11:35:53 +0200558 if flags == [] or 'FIDS' in flags:
559 sels.update({x.fid: x for x in self.parent.children.values() if x.fid and x != self})
560 if flags == [] or 'FNAMES' in flags:
561 sels.update({x.name: x for x in self.parent.children.values() if x.name and x != self})
Harald Welteb2edd142021-01-08 23:29:35 +0100562 return sels
563
564
565class TransparentEF(CardEF):
Harald Welteee3501f2021-04-02 13:00:18 +0200566 """Transparent EF (Entry File) in the smart card filesystem.
567
568 A Transparent EF is a binary file with no formal structure. This is contrary to
569 Record based EFs which have [fixed size] records that can be individually read/updated."""
Harald Welteb2edd142021-01-08 23:29:35 +0100570
571 @with_default_category('Transparent EF Commands')
572 class ShellCommands(CommandSet):
Harald Weltec9cdce32021-04-11 10:28:28 +0200573 """Shell commands specific for transparent EFs."""
Harald Weltec91085e2022-02-10 18:05:45 +0100574
Harald Welteb2edd142021-01-08 23:29:35 +0100575 def __init__(self):
576 super().__init__()
577
Harald Welteaefd0642022-02-25 15:26:37 +0100578 dec_hex_parser = argparse.ArgumentParser()
579 dec_hex_parser.add_argument('--oneline', action='store_true',
580 help='No JSON pretty-printing, dump as a single line')
581 dec_hex_parser.add_argument('HEXSTR', help='Hex-string of encoded data to decode')
582
583 @cmd2.with_argparser(dec_hex_parser)
584 def do_decode_hex(self, opts):
585 """Decode command-line provided hex-string as if it was read from the file."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200586 data = self._cmd.lchan.selected_file.decode_hex(opts.HEXSTR)
Harald Welteaefd0642022-02-25 15:26:37 +0100587 self._cmd.poutput_json(data, opts.oneline)
588
Harald Welteb2edd142021-01-08 23:29:35 +0100589 read_bin_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100590 read_bin_parser.add_argument(
591 '--offset', type=int, default=0, help='Byte offset for start of read')
592 read_bin_parser.add_argument(
593 '--length', type=int, help='Number of bytes to read')
594
Harald Welteb2edd142021-01-08 23:29:35 +0100595 @cmd2.with_argparser(read_bin_parser)
596 def do_read_binary(self, opts):
597 """Read binary data from a transparent EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200598 (data, sw) = self._cmd.lchan.read_binary(opts.length, opts.offset)
Harald Welteb2edd142021-01-08 23:29:35 +0100599 self._cmd.poutput(data)
600
Harald Weltebcad86c2021-04-06 20:08:39 +0200601 read_bin_dec_parser = argparse.ArgumentParser()
602 read_bin_dec_parser.add_argument('--oneline', action='store_true',
603 help='No JSON pretty-printing, dump as a single line')
Harald Weltec91085e2022-02-10 18:05:45 +0100604
Harald Weltebcad86c2021-04-06 20:08:39 +0200605 @cmd2.with_argparser(read_bin_dec_parser)
Harald Welteb2edd142021-01-08 23:29:35 +0100606 def do_read_binary_decoded(self, opts):
607 """Read + decode data from a transparent EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200608 (data, sw) = self._cmd.lchan.read_binary_dec()
Harald Welte1748b932021-04-06 21:12:25 +0200609 self._cmd.poutput_json(data, opts.oneline)
Harald Welteb2edd142021-01-08 23:29:35 +0100610
611 upd_bin_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100612 upd_bin_parser.add_argument(
613 '--offset', type=int, default=0, help='Byte offset for start of read')
614 upd_bin_parser.add_argument(
615 'data', help='Data bytes (hex format) to write')
616
Harald Welteb2edd142021-01-08 23:29:35 +0100617 @cmd2.with_argparser(upd_bin_parser)
618 def do_update_binary(self, opts):
619 """Update (Write) data of a transparent EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200620 (data, sw) = self._cmd.lchan.update_binary(opts.data, opts.offset)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100621 if data:
622 self._cmd.poutput(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100623
624 upd_bin_dec_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100625 upd_bin_dec_parser.add_argument(
626 'data', help='Abstract data (JSON format) to write')
Harald Welte0d4e98a2021-04-07 00:14:40 +0200627 upd_bin_dec_parser.add_argument('--json-path', type=str,
628 help='JSON path to modify specific element of file only')
Harald Weltec91085e2022-02-10 18:05:45 +0100629
Harald Welteb2edd142021-01-08 23:29:35 +0100630 @cmd2.with_argparser(upd_bin_dec_parser)
631 def do_update_binary_decoded(self, opts):
632 """Encode + Update (Write) data of a transparent EF"""
Harald Welte0d4e98a2021-04-07 00:14:40 +0200633 if opts.json_path:
Harald Weltea6c0f882022-07-17 14:23:17 +0200634 (data_json, sw) = self._cmd.lchan.read_binary_dec()
Harald Weltec91085e2022-02-10 18:05:45 +0100635 js_path_modify(data_json, opts.json_path,
636 json.loads(opts.data))
Harald Welte0d4e98a2021-04-07 00:14:40 +0200637 else:
638 data_json = json.loads(opts.data)
Harald Weltea6c0f882022-07-17 14:23:17 +0200639 (data, sw) = self._cmd.lchan.update_binary_dec(data_json)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100640 if data:
Harald Welte1748b932021-04-06 21:12:25 +0200641 self._cmd.poutput_json(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100642
Harald Welte4145d3c2021-04-08 20:34:13 +0200643 def do_edit_binary_decoded(self, opts):
644 """Edit the JSON representation of the EF contents in an editor."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200645 (orig_json, sw) = self._cmd.lchan.read_binary_dec()
Harald Welte4145d3c2021-04-08 20:34:13 +0200646 with tempfile.TemporaryDirectory(prefix='pysim_') as dirname:
647 filename = '%s/file' % dirname
648 # write existing data as JSON to file
649 with open(filename, 'w') as text_file:
650 json.dump(orig_json, text_file, indent=4)
651 # run a text editor
Harald Weltee1268722023-06-24 07:57:08 +0200652 self._cmd.run_editor(filename)
Harald Welte4145d3c2021-04-08 20:34:13 +0200653 with open(filename, 'r') as text_file:
654 edited_json = json.load(text_file)
655 if edited_json == orig_json:
656 self._cmd.poutput("Data not modified, skipping write")
657 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200658 (data, sw) = self._cmd.lchan.update_binary_dec(edited_json)
Harald Welte4145d3c2021-04-08 20:34:13 +0200659 if data:
660 self._cmd.poutput_json(data)
661
Harald Weltec91085e2022-02-10 18:05:45 +0100662 def __init__(self, fid: str, sfid: str = None, name: str = None, desc: str = None, parent: CardDF = None,
Harald Welte13edf302022-07-21 15:19:23 +0200663 size: Size = (1, None), **kwargs):
Harald Welteee3501f2021-04-02 13:00:18 +0200664 """
665 Args:
666 fid : File Identifier (4 hex digits)
667 sfid : Short File Identifier (2 hex digits, optional)
668 name : Brief name of the file, lik EF_ICCID
Harald Weltec9cdce32021-04-11 10:28:28 +0200669 desc : Description of the file
Harald Welteee3501f2021-04-02 13:00:18 +0200670 parent : Parent CardFile object within filesystem hierarchy
671 size : tuple of (minimum_size, recommended_size)
672 """
Harald Welte9170fbf2022-02-11 21:54:37 +0100673 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, **kwargs)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200674 self._construct = None
Harald Weltefb506212021-05-29 21:28:24 +0200675 self._tlv = None
Harald Welteb2edd142021-01-08 23:29:35 +0100676 self.size = size
677 self.shell_commands = [self.ShellCommands()]
678
Harald Weltec91085e2022-02-10 18:05:45 +0100679 def decode_bin(self, raw_bin_data: bytearray) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +0200680 """Decode raw (binary) data into abstract representation.
681
682 A derived class would typically provide a _decode_bin() or _decode_hex() method
683 for implementing this specifically for the given file. This function checks which
684 of the method exists, add calls them (with conversion, as needed).
685
686 Args:
687 raw_bin_data : binary encoded data
688 Returns:
689 abstract_data; dict representing the decoded data
690 """
Harald Welteb2edd142021-01-08 23:29:35 +0100691 method = getattr(self, '_decode_bin', None)
692 if callable(method):
693 return method(raw_bin_data)
694 method = getattr(self, '_decode_hex', None)
695 if callable(method):
696 return method(b2h(raw_bin_data))
Harald Welte2db5cfb2021-04-10 19:05:37 +0200697 if self._construct:
Harald Welte07c7b1f2021-05-28 22:01:29 +0200698 return parse_construct(self._construct, raw_bin_data)
Harald Weltefb506212021-05-29 21:28:24 +0200699 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +0100700 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +0100701 t.from_tlv(raw_bin_data)
702 return t.to_dict()
Harald Welteb2edd142021-01-08 23:29:35 +0100703 return {'raw': raw_bin_data.hex()}
704
Harald Weltec91085e2022-02-10 18:05:45 +0100705 def decode_hex(self, raw_hex_data: str) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +0200706 """Decode raw (hex string) data into abstract representation.
707
708 A derived class would typically provide a _decode_bin() or _decode_hex() method
709 for implementing this specifically for the given file. This function checks which
710 of the method exists, add calls them (with conversion, as needed).
711
712 Args:
713 raw_hex_data : hex-encoded data
714 Returns:
715 abstract_data; dict representing the decoded data
716 """
Harald Welteb2edd142021-01-08 23:29:35 +0100717 method = getattr(self, '_decode_hex', None)
718 if callable(method):
719 return method(raw_hex_data)
720 raw_bin_data = h2b(raw_hex_data)
721 method = getattr(self, '_decode_bin', None)
722 if callable(method):
723 return method(raw_bin_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200724 if self._construct:
Harald Welte07c7b1f2021-05-28 22:01:29 +0200725 return parse_construct(self._construct, raw_bin_data)
Harald Weltefb506212021-05-29 21:28:24 +0200726 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +0100727 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +0100728 t.from_tlv(raw_bin_data)
729 return t.to_dict()
Harald Welteb2edd142021-01-08 23:29:35 +0100730 return {'raw': raw_bin_data.hex()}
731
Harald Weltec91085e2022-02-10 18:05:45 +0100732 def encode_bin(self, abstract_data: dict) -> bytearray:
Harald Welteee3501f2021-04-02 13:00:18 +0200733 """Encode abstract representation into raw (binary) data.
734
735 A derived class would typically provide an _encode_bin() or _encode_hex() method
736 for implementing this specifically for the given file. This function checks which
737 of the method exists, add calls them (with conversion, as needed).
738
739 Args:
740 abstract_data : dict representing the decoded data
741 Returns:
742 binary encoded data
743 """
Harald Welteb2edd142021-01-08 23:29:35 +0100744 method = getattr(self, '_encode_bin', None)
745 if callable(method):
746 return method(abstract_data)
747 method = getattr(self, '_encode_hex', None)
748 if callable(method):
749 return h2b(method(abstract_data))
Harald Welte2db5cfb2021-04-10 19:05:37 +0200750 if self._construct:
751 return self._construct.build(abstract_data)
Harald Weltefb506212021-05-29 21:28:24 +0200752 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +0100753 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +0100754 t.from_dict(abstract_data)
755 return t.to_tlv()
Harald Weltec91085e2022-02-10 18:05:45 +0100756 raise NotImplementedError(
757 "%s encoder not yet implemented. Patches welcome." % self)
Harald Welteb2edd142021-01-08 23:29:35 +0100758
Harald Weltec91085e2022-02-10 18:05:45 +0100759 def encode_hex(self, abstract_data: dict) -> str:
Harald Welteee3501f2021-04-02 13:00:18 +0200760 """Encode abstract representation into raw (hex string) data.
761
762 A derived class would typically provide an _encode_bin() or _encode_hex() method
763 for implementing this specifically for the given file. This function checks which
764 of the method exists, add calls them (with conversion, as needed).
765
766 Args:
767 abstract_data : dict representing the decoded data
768 Returns:
769 hex string encoded data
770 """
Harald Welteb2edd142021-01-08 23:29:35 +0100771 method = getattr(self, '_encode_hex', None)
772 if callable(method):
773 return method(abstract_data)
774 method = getattr(self, '_encode_bin', None)
775 if callable(method):
776 raw_bin_data = method(abstract_data)
777 return b2h(raw_bin_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200778 if self._construct:
779 return b2h(self._construct.build(abstract_data))
Harald Weltefb506212021-05-29 21:28:24 +0200780 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +0100781 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +0100782 t.from_dict(abstract_data)
783 return b2h(t.to_tlv())
Harald Weltec91085e2022-02-10 18:05:45 +0100784 raise NotImplementedError(
785 "%s encoder not yet implemented. Patches welcome." % self)
Harald Welteb2edd142021-01-08 23:29:35 +0100786
787
788class LinFixedEF(CardEF):
Harald Welteee3501f2021-04-02 13:00:18 +0200789 """Linear Fixed EF (Entry File) in the smart card filesystem.
790
791 Linear Fixed EFs are record oriented files. They consist of a number of fixed-size
792 records. The records can be individually read/updated."""
Harald Welteb2edd142021-01-08 23:29:35 +0100793
794 @with_default_category('Linear Fixed EF Commands')
795 class ShellCommands(CommandSet):
Harald Welteee3501f2021-04-02 13:00:18 +0200796 """Shell commands specific for Linear Fixed EFs."""
Harald Weltec91085e2022-02-10 18:05:45 +0100797
Harald Welte9170fbf2022-02-11 21:54:37 +0100798 def __init__(self, **kwargs):
799 super().__init__(**kwargs)
Harald Welteb2edd142021-01-08 23:29:35 +0100800
Harald Welteaefd0642022-02-25 15:26:37 +0100801 dec_hex_parser = argparse.ArgumentParser()
802 dec_hex_parser.add_argument('--oneline', action='store_true',
803 help='No JSON pretty-printing, dump as a single line')
804 dec_hex_parser.add_argument('HEXSTR', help='Hex-string of encoded data to decode')
805
806 @cmd2.with_argparser(dec_hex_parser)
807 def do_decode_hex(self, opts):
808 """Decode command-line provided hex-string as if it was read from the file."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200809 data = self._cmd.lchan.selected_file.decode_record_hex(opts.HEXSTR)
Harald Welteaefd0642022-02-25 15:26:37 +0100810 self._cmd.poutput_json(data, opts.oneline)
811
Harald Welteb2edd142021-01-08 23:29:35 +0100812 read_rec_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100813 read_rec_parser.add_argument(
814 'record_nr', type=int, help='Number of record to be read')
815 read_rec_parser.add_argument(
816 '--count', type=int, default=1, help='Number of records to be read, beginning at record_nr')
817
Harald Welteb2edd142021-01-08 23:29:35 +0100818 @cmd2.with_argparser(read_rec_parser)
819 def do_read_record(self, opts):
Philipp Maier41555732021-02-25 16:52:08 +0100820 """Read one or multiple records from a record-oriented EF"""
821 for r in range(opts.count):
822 recnr = opts.record_nr + r
Harald Weltea6c0f882022-07-17 14:23:17 +0200823 (data, sw) = self._cmd.lchan.read_record(recnr)
Philipp Maier41555732021-02-25 16:52:08 +0100824 if (len(data) > 0):
Harald Weltec91085e2022-02-10 18:05:45 +0100825 recstr = str(data)
Philipp Maier41555732021-02-25 16:52:08 +0100826 else:
Harald Weltec91085e2022-02-10 18:05:45 +0100827 recstr = "(empty)"
Philipp Maier41555732021-02-25 16:52:08 +0100828 self._cmd.poutput("%03d %s" % (recnr, recstr))
Harald Welteb2edd142021-01-08 23:29:35 +0100829
830 read_rec_dec_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100831 read_rec_dec_parser.add_argument(
832 'record_nr', type=int, help='Number of record to be read')
Harald Weltebcad86c2021-04-06 20:08:39 +0200833 read_rec_dec_parser.add_argument('--oneline', action='store_true',
834 help='No JSON pretty-printing, dump as a single line')
Harald Weltec91085e2022-02-10 18:05:45 +0100835
Harald Welteb2edd142021-01-08 23:29:35 +0100836 @cmd2.with_argparser(read_rec_dec_parser)
837 def do_read_record_decoded(self, opts):
838 """Read + decode a record from a record-oriented EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200839 (data, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
Harald Welte1748b932021-04-06 21:12:25 +0200840 self._cmd.poutput_json(data, opts.oneline)
Harald Welteb2edd142021-01-08 23:29:35 +0100841
Harald Welte850b72a2021-04-07 09:33:03 +0200842 read_recs_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100843
Harald Welte850b72a2021-04-07 09:33:03 +0200844 @cmd2.with_argparser(read_recs_parser)
845 def do_read_records(self, opts):
846 """Read all records from a record-oriented EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200847 num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
Harald Welte850b72a2021-04-07 09:33:03 +0200848 for recnr in range(1, 1 + num_of_rec):
Harald Weltea6c0f882022-07-17 14:23:17 +0200849 (data, sw) = self._cmd.lchan.read_record(recnr)
Harald Welte850b72a2021-04-07 09:33:03 +0200850 if (len(data) > 0):
Harald Weltec91085e2022-02-10 18:05:45 +0100851 recstr = str(data)
Harald Welte850b72a2021-04-07 09:33:03 +0200852 else:
Harald Weltec91085e2022-02-10 18:05:45 +0100853 recstr = "(empty)"
Harald Welte850b72a2021-04-07 09:33:03 +0200854 self._cmd.poutput("%03d %s" % (recnr, recstr))
855
856 read_recs_dec_parser = argparse.ArgumentParser()
857 read_recs_dec_parser.add_argument('--oneline', action='store_true',
Harald Weltec91085e2022-02-10 18:05:45 +0100858 help='No JSON pretty-printing, dump as a single line')
859
Harald Welte850b72a2021-04-07 09:33:03 +0200860 @cmd2.with_argparser(read_recs_dec_parser)
861 def do_read_records_decoded(self, opts):
862 """Read + decode all records from a record-oriented EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200863 num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
Harald Welte850b72a2021-04-07 09:33:03 +0200864 # collect all results in list so they are rendered as JSON list when printing
865 data_list = []
866 for recnr in range(1, 1 + num_of_rec):
Harald Weltea6c0f882022-07-17 14:23:17 +0200867 (data, sw) = self._cmd.lchan.read_record_dec(recnr)
Harald Welte850b72a2021-04-07 09:33:03 +0200868 data_list.append(data)
869 self._cmd.poutput_json(data_list, opts.oneline)
870
Harald Welteb2edd142021-01-08 23:29:35 +0100871 upd_rec_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100872 upd_rec_parser.add_argument(
873 'record_nr', type=int, help='Number of record to be read')
874 upd_rec_parser.add_argument(
875 'data', help='Data bytes (hex format) to write')
876
Harald Welteb2edd142021-01-08 23:29:35 +0100877 @cmd2.with_argparser(upd_rec_parser)
878 def do_update_record(self, opts):
879 """Update (write) data to a record-oriented EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200880 (data, sw) = self._cmd.lchan.update_record(opts.record_nr, opts.data)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100881 if data:
882 self._cmd.poutput(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100883
884 upd_rec_dec_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100885 upd_rec_dec_parser.add_argument(
886 'record_nr', type=int, help='Number of record to be read')
887 upd_rec_dec_parser.add_argument(
888 'data', help='Abstract data (JSON format) to write')
Harald Welte0d4e98a2021-04-07 00:14:40 +0200889 upd_rec_dec_parser.add_argument('--json-path', type=str,
890 help='JSON path to modify specific element of record only')
Harald Weltec91085e2022-02-10 18:05:45 +0100891
Harald Welteb2edd142021-01-08 23:29:35 +0100892 @cmd2.with_argparser(upd_rec_dec_parser)
893 def do_update_record_decoded(self, opts):
894 """Encode + Update (write) data to a record-oriented EF"""
Harald Welte0d4e98a2021-04-07 00:14:40 +0200895 if opts.json_path:
Harald Weltea6c0f882022-07-17 14:23:17 +0200896 (data_json, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
Harald Weltec91085e2022-02-10 18:05:45 +0100897 js_path_modify(data_json, opts.json_path,
898 json.loads(opts.data))
Harald Welte0d4e98a2021-04-07 00:14:40 +0200899 else:
900 data_json = json.loads(opts.data)
Harald Weltea6c0f882022-07-17 14:23:17 +0200901 (data, sw) = self._cmd.lchan.update_record_dec(
Harald Weltec91085e2022-02-10 18:05:45 +0100902 opts.record_nr, data_json)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100903 if data:
904 self._cmd.poutput(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100905
Harald Welte4145d3c2021-04-08 20:34:13 +0200906 edit_rec_dec_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100907 edit_rec_dec_parser.add_argument(
908 'record_nr', type=int, help='Number of record to be edited')
909
Harald Welte4145d3c2021-04-08 20:34:13 +0200910 @cmd2.with_argparser(edit_rec_dec_parser)
911 def do_edit_record_decoded(self, opts):
912 """Edit the JSON representation of one record in an editor."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200913 (orig_json, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
Vadim Yanitskiy895fa6f2021-05-02 02:36:44 +0200914 with tempfile.TemporaryDirectory(prefix='pysim_') as dirname:
Harald Welte4145d3c2021-04-08 20:34:13 +0200915 filename = '%s/file' % dirname
916 # write existing data as JSON to file
917 with open(filename, 'w') as text_file:
918 json.dump(orig_json, text_file, indent=4)
919 # run a text editor
Harald Weltee1268722023-06-24 07:57:08 +0200920 self._cmd.run_editor(filename)
Harald Welte4145d3c2021-04-08 20:34:13 +0200921 with open(filename, 'r') as text_file:
922 edited_json = json.load(text_file)
923 if edited_json == orig_json:
924 self._cmd.poutput("Data not modified, skipping write")
925 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200926 (data, sw) = self._cmd.lchan.update_record_dec(
Harald Weltec91085e2022-02-10 18:05:45 +0100927 opts.record_nr, edited_json)
Harald Welte4145d3c2021-04-08 20:34:13 +0200928 if data:
929 self._cmd.poutput_json(data)
Harald Welte4145d3c2021-04-08 20:34:13 +0200930
Harald Weltec91085e2022-02-10 18:05:45 +0100931 def __init__(self, fid: str, sfid: str = None, name: str = None, desc: str = None,
Harald Welte99e4cc02022-07-21 15:25:47 +0200932 parent: Optional[CardDF] = None, rec_len: Size = (1, None), **kwargs):
Harald Welteee3501f2021-04-02 13:00:18 +0200933 """
934 Args:
935 fid : File Identifier (4 hex digits)
936 sfid : Short File Identifier (2 hex digits, optional)
937 name : Brief name of the file, lik EF_ICCID
Harald Weltec9cdce32021-04-11 10:28:28 +0200938 desc : Description of the file
Harald Welteee3501f2021-04-02 13:00:18 +0200939 parent : Parent CardFile object within filesystem hierarchy
Harald Welte99e4cc02022-07-21 15:25:47 +0200940 rec_len : Tuple of (minimum_length, recommended_length)
Harald Welteee3501f2021-04-02 13:00:18 +0200941 """
Harald Welte9170fbf2022-02-11 21:54:37 +0100942 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, **kwargs)
Harald Welteb2edd142021-01-08 23:29:35 +0100943 self.rec_len = rec_len
944 self.shell_commands = [self.ShellCommands()]
Harald Welte2db5cfb2021-04-10 19:05:37 +0200945 self._construct = None
Harald Weltefb506212021-05-29 21:28:24 +0200946 self._tlv = None
Harald Welteb2edd142021-01-08 23:29:35 +0100947
Harald Weltecaa94b52023-01-31 16:43:34 +0100948 def decode_record_hex(self, raw_hex_data: str, record_nr: int = 1) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +0200949 """Decode raw (hex string) data into abstract representation.
950
951 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
952 method for implementing this specifically for the given file. This function checks which
953 of the method exists, add calls them (with conversion, as needed).
954
955 Args:
956 raw_hex_data : hex-encoded data
Harald Weltef6b37af2023-01-24 15:42:26 +0100957 record_nr : record number (1 for first record, ...)
Harald Welteee3501f2021-04-02 13:00:18 +0200958 Returns:
959 abstract_data; dict representing the decoded data
960 """
Harald Welteb2edd142021-01-08 23:29:35 +0100961 method = getattr(self, '_decode_record_hex', None)
962 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +0100963 return method(raw_hex_data, record_nr=record_nr)
Harald Welteb2edd142021-01-08 23:29:35 +0100964 raw_bin_data = h2b(raw_hex_data)
965 method = getattr(self, '_decode_record_bin', None)
966 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +0100967 return method(raw_bin_data, record_nr=record_nr)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200968 if self._construct:
Harald Welte07c7b1f2021-05-28 22:01:29 +0200969 return parse_construct(self._construct, raw_bin_data)
Harald Weltefb506212021-05-29 21:28:24 +0200970 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +0100971 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +0100972 t.from_tlv(raw_bin_data)
973 return t.to_dict()
Harald Welteb2edd142021-01-08 23:29:35 +0100974 return {'raw': raw_bin_data.hex()}
975
Harald Weltef6b37af2023-01-24 15:42:26 +0100976 def decode_record_bin(self, raw_bin_data: bytearray, record_nr: int) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +0200977 """Decode raw (binary) data into abstract representation.
978
979 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
980 method for implementing this specifically for the given file. This function checks which
981 of the method exists, add calls them (with conversion, as needed).
982
983 Args:
984 raw_bin_data : binary encoded data
Harald Weltef6b37af2023-01-24 15:42:26 +0100985 record_nr : record number (1 for first record, ...)
Harald Welteee3501f2021-04-02 13:00:18 +0200986 Returns:
987 abstract_data; dict representing the decoded data
988 """
Harald Welteb2edd142021-01-08 23:29:35 +0100989 method = getattr(self, '_decode_record_bin', None)
990 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +0100991 return method(raw_bin_data, record_nr=record_nr)
Harald Welteb2edd142021-01-08 23:29:35 +0100992 raw_hex_data = b2h(raw_bin_data)
993 method = getattr(self, '_decode_record_hex', None)
994 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +0100995 return method(raw_hex_data, record_nr=record_nr)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200996 if self._construct:
Harald Welte07c7b1f2021-05-28 22:01:29 +0200997 return parse_construct(self._construct, raw_bin_data)
Harald Weltefb506212021-05-29 21:28:24 +0200998 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +0100999 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +01001000 t.from_tlv(raw_bin_data)
1001 return t.to_dict()
Harald Welteb2edd142021-01-08 23:29:35 +01001002 return {'raw': raw_hex_data}
1003
Harald Weltef6b37af2023-01-24 15:42:26 +01001004 def encode_record_hex(self, abstract_data: dict, record_nr: int) -> str:
Harald Welteee3501f2021-04-02 13:00:18 +02001005 """Encode abstract representation into raw (hex string) data.
1006
1007 A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
1008 method for implementing this specifically for the given file. This function checks which
1009 of the method exists, add calls them (with conversion, as needed).
1010
1011 Args:
1012 abstract_data : dict representing the decoded data
Harald Weltef6b37af2023-01-24 15:42:26 +01001013 record_nr : record number (1 for first record, ...)
Harald Welteee3501f2021-04-02 13:00:18 +02001014 Returns:
1015 hex string encoded data
1016 """
Harald Welteb2edd142021-01-08 23:29:35 +01001017 method = getattr(self, '_encode_record_hex', None)
1018 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +01001019 return method(abstract_data, record_nr=record_nr)
Harald Welteb2edd142021-01-08 23:29:35 +01001020 method = getattr(self, '_encode_record_bin', None)
1021 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +01001022 raw_bin_data = method(abstract_data, record_nr=record_nr)
Harald Welte1e456572021-04-02 17:16:30 +02001023 return b2h(raw_bin_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +02001024 if self._construct:
1025 return b2h(self._construct.build(abstract_data))
Harald Weltefb506212021-05-29 21:28:24 +02001026 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +01001027 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +01001028 t.from_dict(abstract_data)
1029 return b2h(t.to_tlv())
Harald Weltec91085e2022-02-10 18:05:45 +01001030 raise NotImplementedError(
1031 "%s encoder not yet implemented. Patches welcome." % self)
Harald Welteb2edd142021-01-08 23:29:35 +01001032
Harald Weltef6b37af2023-01-24 15:42:26 +01001033 def encode_record_bin(self, abstract_data: dict, record_nr : int) -> bytearray:
Harald Welteee3501f2021-04-02 13:00:18 +02001034 """Encode abstract representation into raw (binary) data.
1035
1036 A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
1037 method for implementing this specifically for the given file. This function checks which
1038 of the method exists, add calls them (with conversion, as needed).
1039
1040 Args:
1041 abstract_data : dict representing the decoded data
Harald Weltef6b37af2023-01-24 15:42:26 +01001042 record_nr : record number (1 for first record, ...)
Harald Welteee3501f2021-04-02 13:00:18 +02001043 Returns:
1044 binary encoded data
1045 """
Harald Welteb2edd142021-01-08 23:29:35 +01001046 method = getattr(self, '_encode_record_bin', None)
1047 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +01001048 return method(abstract_data, record_nr=record_nr)
Harald Welteb2edd142021-01-08 23:29:35 +01001049 method = getattr(self, '_encode_record_hex', None)
1050 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +01001051 return h2b(method(abstract_data, record_nr=record_nr))
Harald Welte2db5cfb2021-04-10 19:05:37 +02001052 if self._construct:
1053 return self._construct.build(abstract_data)
Harald Weltefb506212021-05-29 21:28:24 +02001054 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +01001055 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +01001056 t.from_dict(abstract_data)
1057 return t.to_tlv()
Harald Weltec91085e2022-02-10 18:05:45 +01001058 raise NotImplementedError(
1059 "%s encoder not yet implemented. Patches welcome." % self)
1060
Harald Welteb2edd142021-01-08 23:29:35 +01001061
1062class CyclicEF(LinFixedEF):
1063 """Cyclic EF (Entry File) in the smart card filesystem"""
1064 # we don't really have any special support for those; just recycling LinFixedEF here
Harald Weltec91085e2022-02-10 18:05:45 +01001065
1066 def __init__(self, fid: str, sfid: str = None, name: str = None, desc: str = None, parent: CardDF = None,
Harald Welte99e4cc02022-07-21 15:25:47 +02001067 rec_len: Size = (1, None), **kwargs):
Harald Welte9170fbf2022-02-11 21:54:37 +01001068 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, rec_len=rec_len, **kwargs)
Harald Weltec91085e2022-02-10 18:05:45 +01001069
Harald Welteb2edd142021-01-08 23:29:35 +01001070
1071class TransRecEF(TransparentEF):
1072 """Transparent EF (Entry File) containing fixed-size records.
Harald Welteee3501f2021-04-02 13:00:18 +02001073
Harald Welteb2edd142021-01-08 23:29:35 +01001074 These are the real odd-balls and mostly look like mistakes in the specification:
1075 Specified as 'transparent' EF, but actually containing several fixed-length records
1076 inside.
1077 We add a special class for those, so the user only has to provide encoder/decoder functions
1078 for a record, while this class takes care of split / merge of records.
1079 """
Harald Weltec91085e2022-02-10 18:05:45 +01001080
1081 def __init__(self, fid: str, rec_len: int, sfid: str = None, name: str = None, desc: str = None,
Harald Welte13edf302022-07-21 15:19:23 +02001082 parent: Optional[CardDF] = None, size: Size = (1, None), **kwargs):
Harald Welteee3501f2021-04-02 13:00:18 +02001083 """
1084 Args:
1085 fid : File Identifier (4 hex digits)
1086 sfid : Short File Identifier (2 hex digits, optional)
Harald Weltec9cdce32021-04-11 10:28:28 +02001087 name : Brief name of the file, like EF_ICCID
1088 desc : Description of the file
Harald Welteee3501f2021-04-02 13:00:18 +02001089 parent : Parent CardFile object within filesystem hierarchy
1090 rec_len : Length of the fixed-length records within transparent EF
1091 size : tuple of (minimum_size, recommended_size)
1092 """
Harald Welte9170fbf2022-02-11 21:54:37 +01001093 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, size=size, **kwargs)
Harald Welteb2edd142021-01-08 23:29:35 +01001094 self.rec_len = rec_len
1095
Harald Weltec91085e2022-02-10 18:05:45 +01001096 def decode_record_hex(self, raw_hex_data: str) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +02001097 """Decode raw (hex string) data into abstract representation.
1098
1099 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
1100 method for implementing this specifically for the given file. This function checks which
1101 of the method exists, add calls them (with conversion, as needed).
1102
1103 Args:
1104 raw_hex_data : hex-encoded data
1105 Returns:
1106 abstract_data; dict representing the decoded data
1107 """
Harald Welteb2edd142021-01-08 23:29:35 +01001108 method = getattr(self, '_decode_record_hex', None)
1109 if callable(method):
1110 return method(raw_hex_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +02001111 raw_bin_data = h2b(raw_hex_data)
Harald Welteb2edd142021-01-08 23:29:35 +01001112 method = getattr(self, '_decode_record_bin', None)
1113 if callable(method):
Harald Welteb2edd142021-01-08 23:29:35 +01001114 return method(raw_bin_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +02001115 if self._construct:
Harald Welte07c7b1f2021-05-28 22:01:29 +02001116 return parse_construct(self._construct, raw_bin_data)
Harald Weltefb506212021-05-29 21:28:24 +02001117 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +01001118 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +01001119 t.from_tlv(raw_bin_data)
1120 return t.to_dict()
Harald Welteb2edd142021-01-08 23:29:35 +01001121 return {'raw': raw_hex_data}
1122
Harald Weltec91085e2022-02-10 18:05:45 +01001123 def decode_record_bin(self, raw_bin_data: bytearray) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +02001124 """Decode raw (binary) data into abstract representation.
1125
1126 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
1127 method for implementing this specifically for the given file. This function checks which
1128 of the method exists, add calls them (with conversion, as needed).
1129
1130 Args:
1131 raw_bin_data : binary encoded data
1132 Returns:
1133 abstract_data; dict representing the decoded data
1134 """
Harald Welteb2edd142021-01-08 23:29:35 +01001135 method = getattr(self, '_decode_record_bin', None)
1136 if callable(method):
1137 return method(raw_bin_data)
1138 raw_hex_data = b2h(raw_bin_data)
1139 method = getattr(self, '_decode_record_hex', None)
1140 if callable(method):
1141 return method(raw_hex_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +02001142 if self._construct:
Harald Welte07c7b1f2021-05-28 22:01:29 +02001143 return parse_construct(self._construct, raw_bin_data)
Harald Weltefb506212021-05-29 21:28:24 +02001144 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +01001145 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +01001146 t.from_tlv(raw_bin_data)
1147 return t.to_dict()
Harald Welteb2edd142021-01-08 23:29:35 +01001148 return {'raw': raw_hex_data}
1149
Harald Weltec91085e2022-02-10 18:05:45 +01001150 def encode_record_hex(self, abstract_data: dict) -> str:
Harald Welteee3501f2021-04-02 13:00:18 +02001151 """Encode abstract representation into raw (hex string) data.
1152
1153 A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
1154 method for implementing this specifically for the given file. This function checks which
1155 of the method exists, add calls them (with conversion, as needed).
1156
1157 Args:
1158 abstract_data : dict representing the decoded data
1159 Returns:
1160 hex string encoded data
1161 """
Harald Welteb2edd142021-01-08 23:29:35 +01001162 method = getattr(self, '_encode_record_hex', None)
1163 if callable(method):
1164 return method(abstract_data)
1165 method = getattr(self, '_encode_record_bin', None)
1166 if callable(method):
Harald Welte1e456572021-04-02 17:16:30 +02001167 return b2h(method(abstract_data))
Harald Welte2db5cfb2021-04-10 19:05:37 +02001168 if self._construct:
1169 return b2h(filter_dict(self._construct.build(abstract_data)))
Harald Weltefb506212021-05-29 21:28:24 +02001170 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +01001171 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +01001172 t.from_dict(abstract_data)
1173 return b2h(t.to_tlv())
Harald Weltec91085e2022-02-10 18:05:45 +01001174 raise NotImplementedError(
1175 "%s encoder not yet implemented. Patches welcome." % self)
Harald Welteb2edd142021-01-08 23:29:35 +01001176
Harald Weltec91085e2022-02-10 18:05:45 +01001177 def encode_record_bin(self, abstract_data: dict) -> bytearray:
Harald Welteee3501f2021-04-02 13:00:18 +02001178 """Encode abstract representation into raw (binary) data.
1179
1180 A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
1181 method for implementing this specifically for the given file. This function checks which
1182 of the method exists, add calls them (with conversion, as needed).
1183
1184 Args:
1185 abstract_data : dict representing the decoded data
1186 Returns:
1187 binary encoded data
1188 """
Harald Welteb2edd142021-01-08 23:29:35 +01001189 method = getattr(self, '_encode_record_bin', None)
1190 if callable(method):
1191 return method(abstract_data)
1192 method = getattr(self, '_encode_record_hex', None)
1193 if callable(method):
1194 return h2b(method(abstract_data))
Harald Welte2db5cfb2021-04-10 19:05:37 +02001195 if self._construct:
1196 return filter_dict(self._construct.build(abstract_data))
Harald Weltefb506212021-05-29 21:28:24 +02001197 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +01001198 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +01001199 t.from_dict(abstract_data)
1200 return t.to_tlv()
Harald Weltec91085e2022-02-10 18:05:45 +01001201 raise NotImplementedError(
1202 "%s encoder not yet implemented. Patches welcome." % self)
Harald Welteb2edd142021-01-08 23:29:35 +01001203
Harald Weltec91085e2022-02-10 18:05:45 +01001204 def _decode_bin(self, raw_bin_data: bytearray):
1205 chunks = [raw_bin_data[i:i+self.rec_len]
1206 for i in range(0, len(raw_bin_data), self.rec_len)]
Harald Welteb2edd142021-01-08 23:29:35 +01001207 return [self.decode_record_bin(x) for x in chunks]
1208
Harald Welteee3501f2021-04-02 13:00:18 +02001209 def _encode_bin(self, abstract_data) -> bytes:
Harald Welteb2edd142021-01-08 23:29:35 +01001210 chunks = [self.encode_record_bin(x) for x in abstract_data]
1211 # FIXME: pad to file size
1212 return b''.join(chunks)
1213
1214
Harald Welte917d98c2021-04-21 11:51:25 +02001215class BerTlvEF(CardEF):
Harald Welte27881622021-04-21 11:16:31 +02001216 """BER-TLV EF (Entry File) in the smart card filesystem.
1217 A BER-TLV EF is a binary file with a BER (Basic Encoding Rules) TLV structure
Harald Welteb2edd142021-01-08 23:29:35 +01001218
Harald Welte27881622021-04-21 11:16:31 +02001219 NOTE: We currently don't really support those, this class is simply a wrapper
1220 around TransparentEF as a place-holder, so we can already define EFs of BER-TLV
1221 type without fully supporting them."""
Harald Welteb2edd142021-01-08 23:29:35 +01001222
Harald Welte917d98c2021-04-21 11:51:25 +02001223 @with_default_category('BER-TLV EF Commands')
1224 class ShellCommands(CommandSet):
1225 """Shell commands specific for BER-TLV EFs."""
Harald Weltec91085e2022-02-10 18:05:45 +01001226
Harald Welte917d98c2021-04-21 11:51:25 +02001227 def __init__(self):
1228 super().__init__()
1229
1230 retrieve_data_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +01001231 retrieve_data_parser.add_argument(
1232 'tag', type=auto_int, help='BER-TLV Tag of value to retrieve')
1233
Harald Welte917d98c2021-04-21 11:51:25 +02001234 @cmd2.with_argparser(retrieve_data_parser)
1235 def do_retrieve_data(self, opts):
1236 """Retrieve (Read) data from a BER-TLV EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +02001237 (data, sw) = self._cmd.lchan.retrieve_data(opts.tag)
Harald Welte917d98c2021-04-21 11:51:25 +02001238 self._cmd.poutput(data)
1239
1240 def do_retrieve_tags(self, opts):
1241 """List tags available in a given BER-TLV EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +02001242 tags = self._cmd.lchan.retrieve_tags()
Harald Welte917d98c2021-04-21 11:51:25 +02001243 self._cmd.poutput(tags)
1244
1245 set_data_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +01001246 set_data_parser.add_argument(
1247 'tag', type=auto_int, help='BER-TLV Tag of value to set')
1248 set_data_parser.add_argument(
1249 'data', help='Data bytes (hex format) to write')
1250
Harald Welte917d98c2021-04-21 11:51:25 +02001251 @cmd2.with_argparser(set_data_parser)
1252 def do_set_data(self, opts):
1253 """Set (Write) data for a given tag in a BER-TLV EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +02001254 (data, sw) = self._cmd.lchan.set_data(opts.tag, opts.data)
Harald Welte917d98c2021-04-21 11:51:25 +02001255 if data:
1256 self._cmd.poutput(data)
1257
1258 del_data_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +01001259 del_data_parser.add_argument(
1260 'tag', type=auto_int, help='BER-TLV Tag of value to set')
1261
Harald Welte917d98c2021-04-21 11:51:25 +02001262 @cmd2.with_argparser(del_data_parser)
1263 def do_delete_data(self, opts):
1264 """Delete data for a given tag in a BER-TLV EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +02001265 (data, sw) = self._cmd.lchan.set_data(opts.tag, None)
Harald Welte917d98c2021-04-21 11:51:25 +02001266 if data:
1267 self._cmd.poutput(data)
1268
Harald Weltec91085e2022-02-10 18:05:45 +01001269 def __init__(self, fid: str, sfid: str = None, name: str = None, desc: str = None, parent: CardDF = None,
Harald Welte13edf302022-07-21 15:19:23 +02001270 size: Size = (1, None), **kwargs):
Harald Welte917d98c2021-04-21 11:51:25 +02001271 """
1272 Args:
1273 fid : File Identifier (4 hex digits)
1274 sfid : Short File Identifier (2 hex digits, optional)
1275 name : Brief name of the file, lik EF_ICCID
1276 desc : Description of the file
1277 parent : Parent CardFile object within filesystem hierarchy
1278 size : tuple of (minimum_size, recommended_size)
1279 """
Harald Welte9170fbf2022-02-11 21:54:37 +01001280 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, **kwargs)
Harald Welte917d98c2021-04-21 11:51:25 +02001281 self._construct = None
1282 self.size = size
1283 self.shell_commands = [self.ShellCommands()]
1284
Harald Welteb2edd142021-01-08 23:29:35 +01001285
Vadim Yanitskiy04b5d9d2022-07-07 03:05:30 +07001286class RuntimeState:
Harald Welteb2edd142021-01-08 23:29:35 +01001287 """Represent the runtime state of a session with a card."""
Harald Weltec91085e2022-02-10 18:05:45 +01001288
1289 def __init__(self, card, profile: 'CardProfile'):
Harald Welteee3501f2021-04-02 13:00:18 +02001290 """
1291 Args:
1292 card : pysim.cards.Card instance
1293 profile : CardProfile instance
1294 """
Philipp Maier5af7bdf2021-11-04 12:48:41 +01001295 self.mf = CardMF(profile=profile)
Harald Welteb2edd142021-01-08 23:29:35 +01001296 self.card = card
Harald Welteb2edd142021-01-08 23:29:35 +01001297 self.profile = profile
Harald Weltea6c0f882022-07-17 14:23:17 +02001298 self.lchan = {}
1299 # the basic logical channel always exists
1300 self.lchan[0] = RuntimeLchan(0, self)
Philipp Maier51cad0d2021-11-08 15:45:10 +01001301
1302 # make sure the class and selection control bytes, which are specified
1303 # by the card profile are used
Harald Weltec91085e2022-02-10 18:05:45 +01001304 self.card.set_apdu_parameter(
1305 cla=self.profile.cla, sel_ctrl=self.profile.sel_ctrl)
Philipp Maier51cad0d2021-11-08 15:45:10 +01001306
Harald Welte5ce35242021-04-02 20:27:05 +02001307 # add application ADFs + MF-files from profile
Philipp Maier1e896f32021-03-10 17:02:53 +01001308 apps = self._match_applications()
1309 for a in apps:
Harald Welte5ce35242021-04-02 20:27:05 +02001310 if a.adf:
1311 self.mf.add_application_df(a.adf)
Harald Welteb2edd142021-01-08 23:29:35 +01001312 for f in self.profile.files_in_mf:
1313 self.mf.add_file(f)
Philipp Maier38c74f62021-03-17 17:19:52 +01001314 self.conserve_write = True
Harald Welteb2edd142021-01-08 23:29:35 +01001315
Philipp Maier4e2e1d92021-11-08 15:36:01 +01001316 # make sure that when the runtime state is created, the card is also
1317 # in a defined state.
1318 self.reset()
1319
Philipp Maier1e896f32021-03-10 17:02:53 +01001320 def _match_applications(self):
1321 """match the applications from the profile with applications on the card"""
1322 apps_profile = self.profile.applications
Philipp Maierd454fe72021-11-08 15:32:23 +01001323
1324 # When the profile does not feature any applications, then we are done already
1325 if not apps_profile:
1326 return []
1327
1328 # Read AIDs from card and match them against the applications defined by the
1329 # card profile
Philipp Maier1e896f32021-03-10 17:02:53 +01001330 aids_card = self.card.read_aids()
1331 apps_taken = []
1332 if aids_card:
1333 aids_taken = []
1334 print("AIDs on card:")
1335 for a in aids_card:
1336 for f in apps_profile:
1337 if f.aid in a:
Philipp Maier8d8bdef2021-12-01 11:48:27 +01001338 print(" %s: %s (EF.DIR)" % (f.name, a))
Philipp Maier1e896f32021-03-10 17:02:53 +01001339 aids_taken.append(a)
1340 apps_taken.append(f)
1341 aids_unknown = set(aids_card) - set(aids_taken)
1342 for a in aids_unknown:
Philipp Maier8d8bdef2021-12-01 11:48:27 +01001343 print(" unknown: %s (EF.DIR)" % a)
Philipp Maier1e896f32021-03-10 17:02:53 +01001344 else:
Philipp Maier8d8bdef2021-12-01 11:48:27 +01001345 print("warning: EF.DIR seems to be empty!")
1346
1347 # Some card applications may not be registered in EF.DIR, we will actively
1348 # probe for those applications
1349 for f in set(apps_profile) - set(apps_taken):
Bjoern Riemerda57ef12022-01-18 15:38:14 +01001350 try:
1351 data, sw = self.card.select_adf_by_aid(f.aid)
1352 if sw == "9000":
1353 print(" %s: %s" % (f.name, f.aid))
1354 apps_taken.append(f)
Tobias Engeld70ac222023-05-29 21:20:59 +02001355 except (SwMatchError, ProtocolError):
Bjoern Riemerda57ef12022-01-18 15:38:14 +01001356 pass
Philipp Maier1e896f32021-03-10 17:02:53 +01001357 return apps_taken
1358
Harald Weltea6c0f882022-07-17 14:23:17 +02001359 def reset(self, cmd_app=None) -> Hexstr:
1360 """Perform physical card reset and obtain ATR.
1361 Args:
1362 cmd_app : Command Application State (for unregistering old file commands)
1363 """
1364 # delete all lchan != 0 (basic lchan)
1365 for lchan_nr in self.lchan.keys():
1366 if lchan_nr == 0:
1367 continue
1368 del self.lchan[lchan_nr]
1369 atr = i2h(self.card.reset())
1370 # select MF to reset internal state and to verify card really works
1371 self.lchan[0].select('MF', cmd_app)
1372 self.lchan[0].selected_adf = None
1373 return atr
1374
1375 def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan':
1376 """Add a logical channel to the runtime state. You shouldn't call this
1377 directly but always go through RuntimeLchan.add_lchan()."""
1378 if lchan_nr in self.lchan.keys():
1379 raise ValueError('Cannot create already-existing lchan %d' % lchan_nr)
1380 self.lchan[lchan_nr] = RuntimeLchan(lchan_nr, self)
1381 return self.lchan[lchan_nr]
1382
1383 def del_lchan(self, lchan_nr: int):
1384 if lchan_nr in self.lchan.keys():
1385 del self.lchan[lchan_nr]
1386 return True
1387 else:
1388 return False
1389
1390 def get_lchan_by_cla(self, cla) -> Optional['RuntimeLchan']:
1391 lchan_nr = lchan_nr_from_cla(cla)
1392 if lchan_nr in self.lchan.keys():
1393 return self.lchan[lchan_nr]
1394 else:
1395 return None
1396
1397
1398class RuntimeLchan:
1399 """Represent the runtime state of a logical channel with a card."""
1400
1401 def __init__(self, lchan_nr: int, rs: RuntimeState):
1402 self.lchan_nr = lchan_nr
1403 self.rs = rs
1404 self.selected_file = self.rs.mf
1405 self.selected_adf = None
1406 self.selected_file_fcp = None
1407 self.selected_file_fcp_hex = None
1408
1409 def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan':
1410 """Add a new logical channel from the current logical channel. Just affects
1411 internal state, doesn't actually open a channel with the UICC."""
1412 new_lchan = self.rs.add_lchan(lchan_nr)
1413 # See TS 102 221 Table 8.3
1414 if self.lchan_nr != 0:
1415 new_lchan.selected_file = self.get_cwd()
1416 new_lchan.selected_adf = self.selected_adf
1417 return new_lchan
1418
Harald Welte747a9782022-02-13 17:52:28 +01001419 def selected_file_descriptor_byte(self) -> dict:
1420 return self.selected_file_fcp['file_descriptor']['file_descriptor_byte']
1421
1422 def selected_file_shareable(self) -> bool:
1423 return self.selected_file_descriptor_byte()['shareable']
1424
1425 def selected_file_structure(self) -> str:
1426 return self.selected_file_descriptor_byte()['structure']
1427
1428 def selected_file_type(self) -> str:
1429 return self.selected_file_descriptor_byte()['file_type']
1430
1431 def selected_file_num_of_rec(self) -> Optional[int]:
1432 return self.selected_file_fcp['file_descriptor'].get('num_of_rec')
1433
Harald Welteee3501f2021-04-02 13:00:18 +02001434 def get_cwd(self) -> CardDF:
1435 """Obtain the current working directory.
1436
1437 Returns:
1438 CardDF instance
1439 """
Harald Welteb2edd142021-01-08 23:29:35 +01001440 if isinstance(self.selected_file, CardDF):
1441 return self.selected_file
1442 else:
1443 return self.selected_file.parent
1444
Harald Welte5ce35242021-04-02 20:27:05 +02001445 def get_application_df(self) -> Optional[CardADF]:
1446 """Obtain the currently selected application DF (if any).
Harald Welteee3501f2021-04-02 13:00:18 +02001447
1448 Returns:
1449 CardADF() instance or None"""
Harald Welteb2edd142021-01-08 23:29:35 +01001450 # iterate upwards from selected file; check if any is an ADF
1451 node = self.selected_file
1452 while node.parent != node:
1453 if isinstance(node, CardADF):
1454 return node
1455 node = node.parent
1456 return None
1457
Harald Weltec91085e2022-02-10 18:05:45 +01001458 def interpret_sw(self, sw: str):
Harald Welteee3501f2021-04-02 13:00:18 +02001459 """Interpret a given status word relative to the currently selected application
1460 or the underlying card profile.
1461
1462 Args:
Harald Weltec9cdce32021-04-11 10:28:28 +02001463 sw : Status word as string of 4 hex digits
Harald Welteee3501f2021-04-02 13:00:18 +02001464
1465 Returns:
1466 Tuple of two strings
1467 """
Harald Welte86fbd392021-04-02 22:13:09 +02001468 res = None
Harald Welte5ce35242021-04-02 20:27:05 +02001469 adf = self.get_application_df()
1470 if adf:
1471 app = adf.application
Harald Welteb2edd142021-01-08 23:29:35 +01001472 # The application either comes with its own interpret_sw
1473 # method or we will use the interpret_sw method from the
1474 # card profile.
Harald Welte5ce35242021-04-02 20:27:05 +02001475 if app and hasattr(app, "interpret_sw"):
Harald Welte86fbd392021-04-02 22:13:09 +02001476 res = app.interpret_sw(sw)
Harald Weltea6c0f882022-07-17 14:23:17 +02001477 return res or self.rs.profile.interpret_sw(sw)
Harald Welteb2edd142021-01-08 23:29:35 +01001478
Harald Weltec91085e2022-02-10 18:05:45 +01001479 def probe_file(self, fid: str, cmd_app=None):
Harald Welteee3501f2021-04-02 13:00:18 +02001480 """Blindly try to select a file and automatically add a matching file
Harald Weltec91085e2022-02-10 18:05:45 +01001481 object if the file actually exists."""
Philipp Maier63f572d2021-03-09 22:42:47 +01001482 if not is_hex(fid, 4, 4):
Harald Weltec91085e2022-02-10 18:05:45 +01001483 raise ValueError(
1484 "Cannot select unknown file by name %s, only hexadecimal 4 digit FID is allowed" % fid)
Philipp Maier63f572d2021-03-09 22:42:47 +01001485
1486 try:
Harald Weltea6c0f882022-07-17 14:23:17 +02001487 (data, sw) = self.rs.card._scc.select_file(fid)
Philipp Maier63f572d2021-03-09 22:42:47 +01001488 except SwMatchError as swm:
1489 k = self.interpret_sw(swm.sw_actual)
1490 if not k:
1491 raise(swm)
1492 raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1]))
1493
1494 select_resp = self.selected_file.decode_select_response(data)
Harald Welte747a9782022-02-13 17:52:28 +01001495 if (select_resp['file_descriptor']['file_descriptor_byte']['file_type'] == 'df'):
Harald Weltec91085e2022-02-10 18:05:45 +01001496 f = CardDF(fid=fid, sfid=None, name="DF." + str(fid).upper(),
1497 desc="dedicated file, manually added at runtime")
Philipp Maier63f572d2021-03-09 22:42:47 +01001498 else:
Harald Welte747a9782022-02-13 17:52:28 +01001499 if (select_resp['file_descriptor']['file_descriptor_byte']['structure'] == 'transparent'):
Harald Weltec91085e2022-02-10 18:05:45 +01001500 f = TransparentEF(fid=fid, sfid=None, name="EF." + str(fid).upper(),
1501 desc="elementary file, manually added at runtime")
Philipp Maier63f572d2021-03-09 22:42:47 +01001502 else:
Harald Weltec91085e2022-02-10 18:05:45 +01001503 f = LinFixedEF(fid=fid, sfid=None, name="EF." + str(fid).upper(),
1504 desc="elementary file, manually added at runtime")
Philipp Maier63f572d2021-03-09 22:42:47 +01001505
1506 self.selected_file.add_files([f])
1507 self.selected_file = f
Philipp Maier6b8eedc2022-06-01 18:10:04 +02001508 return select_resp, data
Philipp Maier63f572d2021-03-09 22:42:47 +01001509
Harald Welteaceb2a52022-02-12 21:41:59 +01001510 def _select_pre(self, cmd_app):
1511 # unregister commands of old file
1512 if cmd_app and self.selected_file.shell_commands:
1513 for c in self.selected_file.shell_commands:
1514 cmd_app.unregister_command_set(c)
1515
1516 def _select_post(self, cmd_app):
1517 # register commands of new file
1518 if cmd_app and self.selected_file.shell_commands:
1519 for c in self.selected_file.shell_commands:
1520 cmd_app.register_command_set(c)
1521
1522 def select_file(self, file: CardFile, cmd_app=None):
1523 """Select a file (EF, DF, ADF, MF, ...).
1524
1525 Args:
1526 file : CardFile [or derived class] instance
1527 cmd_app : Command Application State (for unregistering old file commands)
1528 """
1529 # we need to find a path from our self.selected_file to the destination
1530 inter_path = self.selected_file.build_select_path_to(file)
1531 if not inter_path:
1532 raise RuntimeError('Cannot determine path from %s to %s' % (self.selected_file, file))
1533
1534 self._select_pre(cmd_app)
1535
1536 for p in inter_path:
1537 try:
1538 if isinstance(p, CardADF):
Harald Weltea6c0f882022-07-17 14:23:17 +02001539 (data, sw) = self.rs.card.select_adf_by_aid(p.aid)
Harald Welte46a7a3f2022-07-16 11:47:47 +02001540 self.selected_adf = p
Harald Welteaceb2a52022-02-12 21:41:59 +01001541 else:
Harald Weltea6c0f882022-07-17 14:23:17 +02001542 (data, sw) = self.rs.card._scc.select_file(p.fid)
Harald Welteaceb2a52022-02-12 21:41:59 +01001543 self.selected_file = p
1544 except SwMatchError as swm:
1545 self._select_post(cmd_app)
1546 raise(swm)
1547
1548 self._select_post(cmd_app)
1549
Harald Weltec91085e2022-02-10 18:05:45 +01001550 def select(self, name: str, cmd_app=None):
Harald Welteee3501f2021-04-02 13:00:18 +02001551 """Select a file (EF, DF, ADF, MF, ...).
1552
1553 Args:
1554 name : Name of file to select
1555 cmd_app : Command Application State (for unregistering old file commands)
1556 """
Harald Welteee670bc2022-02-13 15:10:15 +01001557 # handling of entire paths with multiple directories/elements
1558 if '/' in name:
1559 prev_sel_file = self.selected_file
1560 pathlist = name.split('/')
1561 # treat /DF.GSM/foo like MF/DF.GSM/foo
1562 if pathlist[0] == '':
1563 pathlist[0] = 'MF'
1564 try:
1565 for p in pathlist:
1566 self.select(p, cmd_app)
1567 return
1568 except Exception as e:
1569 # if any intermediate step fails, go back to where we were
1570 self.select_file(prev_sel_file, cmd_app)
1571 raise e
1572
Harald Welteb2edd142021-01-08 23:29:35 +01001573 sels = self.selected_file.get_selectables()
Philipp Maier7744b6e2021-03-11 14:29:37 +01001574 if is_hex(name):
1575 name = name.lower()
Philipp Maier63f572d2021-03-09 22:42:47 +01001576
Harald Welteaceb2a52022-02-12 21:41:59 +01001577 self._select_pre(cmd_app)
Philipp Maier63f572d2021-03-09 22:42:47 +01001578
Harald Welteb2edd142021-01-08 23:29:35 +01001579 if name in sels:
1580 f = sels[name]
Harald Welteb2edd142021-01-08 23:29:35 +01001581 try:
1582 if isinstance(f, CardADF):
Harald Weltea6c0f882022-07-17 14:23:17 +02001583 (data, sw) = self.rs.card.select_adf_by_aid(f.aid)
Harald Welteb2edd142021-01-08 23:29:35 +01001584 else:
Harald Weltea6c0f882022-07-17 14:23:17 +02001585 (data, sw) = self.rs.card._scc.select_file(f.fid)
Harald Welteb2edd142021-01-08 23:29:35 +01001586 self.selected_file = f
1587 except SwMatchError as swm:
1588 k = self.interpret_sw(swm.sw_actual)
1589 if not k:
1590 raise(swm)
1591 raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1]))
Philipp Maier63f572d2021-03-09 22:42:47 +01001592 select_resp = f.decode_select_response(data)
Harald Welteb2edd142021-01-08 23:29:35 +01001593 else:
Philipp Maier6b8eedc2022-06-01 18:10:04 +02001594 (select_resp, data) = self.probe_file(name, cmd_app)
1595
Harald Welte2bb17f32022-02-15 15:41:55 +01001596 # store the raw + decoded FCP for later reference
1597 self.selected_file_fcp_hex = data
Harald Welte850b72a2021-04-07 09:33:03 +02001598 self.selected_file_fcp = select_resp
Philipp Maier63f572d2021-03-09 22:42:47 +01001599
Harald Welteaceb2a52022-02-12 21:41:59 +01001600 self._select_post(cmd_app)
Philipp Maier63f572d2021-03-09 22:42:47 +01001601 return select_resp
Harald Welteb2edd142021-01-08 23:29:35 +01001602
Harald Welte34b05d32021-05-25 22:03:13 +02001603 def status(self):
1604 """Request STATUS (current selected file FCP) from card."""
Harald Weltea6c0f882022-07-17 14:23:17 +02001605 (data, sw) = self.rs.card._scc.status()
Harald Welte34b05d32021-05-25 22:03:13 +02001606 return self.selected_file.decode_select_response(data)
1607
Harald Welte3c9b7842021-10-19 21:44:24 +02001608 def get_file_for_selectable(self, name: str):
1609 sels = self.selected_file.get_selectables()
1610 return sels[name]
1611
Harald Weltec91085e2022-02-10 18:05:45 +01001612 def activate_file(self, name: str):
Harald Welte485692b2021-05-25 22:21:44 +02001613 """Request ACTIVATE FILE of specified file."""
1614 sels = self.selected_file.get_selectables()
1615 f = sels[name]
Harald Weltea6c0f882022-07-17 14:23:17 +02001616 data, sw = self.rs.card._scc.activate_file(f.fid)
Harald Welte485692b2021-05-25 22:21:44 +02001617 return data, sw
1618
Harald Weltec91085e2022-02-10 18:05:45 +01001619 def read_binary(self, length: int = None, offset: int = 0):
Harald Welteee3501f2021-04-02 13:00:18 +02001620 """Read [part of] a transparent EF binary data.
1621
1622 Args:
1623 length : Amount of data to read (None: as much as possible)
1624 offset : Offset into the file from which to read 'length' bytes
1625 Returns:
1626 binary data read from the file
1627 """
Harald Welteb2edd142021-01-08 23:29:35 +01001628 if not isinstance(self.selected_file, TransparentEF):
1629 raise TypeError("Only works with TransparentEF")
Harald Weltea6c0f882022-07-17 14:23:17 +02001630 return self.rs.card._scc.read_binary(self.selected_file.fid, length, offset)
Harald Welteb2edd142021-01-08 23:29:35 +01001631
Harald Welte2d4a64b2021-04-03 09:01:24 +02001632 def read_binary_dec(self) -> Tuple[dict, str]:
Harald Welteee3501f2021-04-02 13:00:18 +02001633 """Read [part of] a transparent EF binary data and decode it.
1634
1635 Args:
1636 length : Amount of data to read (None: as much as possible)
1637 offset : Offset into the file from which to read 'length' bytes
1638 Returns:
1639 abstract decode data read from the file
1640 """
Harald Welteb2edd142021-01-08 23:29:35 +01001641 (data, sw) = self.read_binary()
1642 dec_data = self.selected_file.decode_hex(data)
Harald Welteb2edd142021-01-08 23:29:35 +01001643 return (dec_data, sw)
1644
Harald Weltec91085e2022-02-10 18:05:45 +01001645 def update_binary(self, data_hex: str, offset: int = 0):
Harald Welteee3501f2021-04-02 13:00:18 +02001646 """Update transparent EF binary data.
1647
1648 Args:
1649 data_hex : hex string of data to be written
1650 offset : Offset into the file from which to write 'data_hex'
1651 """
Harald Welteb2edd142021-01-08 23:29:35 +01001652 if not isinstance(self.selected_file, TransparentEF):
1653 raise TypeError("Only works with TransparentEF")
Harald Weltea6c0f882022-07-17 14:23:17 +02001654 return self.rs.card._scc.update_binary(self.selected_file.fid, data_hex, offset, conserve=self.rs.conserve_write)
Harald Welteb2edd142021-01-08 23:29:35 +01001655
Harald Weltec91085e2022-02-10 18:05:45 +01001656 def update_binary_dec(self, data: dict):
Harald Welteee3501f2021-04-02 13:00:18 +02001657 """Update transparent EF from abstract data. Encodes the data to binary and
1658 then updates the EF with it.
1659
1660 Args:
1661 data : abstract data which is to be encoded and written
1662 """
Harald Welteb2edd142021-01-08 23:29:35 +01001663 data_hex = self.selected_file.encode_hex(data)
Harald Welteb2edd142021-01-08 23:29:35 +01001664 return self.update_binary(data_hex)
1665
Harald Weltec91085e2022-02-10 18:05:45 +01001666 def read_record(self, rec_nr: int = 0):
Harald Welteee3501f2021-04-02 13:00:18 +02001667 """Read a record as binary data.
1668
1669 Args:
1670 rec_nr : Record number to read
1671 Returns:
1672 hex string of binary data contained in record
1673 """
Harald Welteb2edd142021-01-08 23:29:35 +01001674 if not isinstance(self.selected_file, LinFixedEF):
1675 raise TypeError("Only works with Linear Fixed EF")
1676 # returns a string of hex nibbles
Harald Weltea6c0f882022-07-17 14:23:17 +02001677 return self.rs.card._scc.read_record(self.selected_file.fid, rec_nr)
Harald Welteb2edd142021-01-08 23:29:35 +01001678
Harald Weltec91085e2022-02-10 18:05:45 +01001679 def read_record_dec(self, rec_nr: int = 0) -> Tuple[dict, str]:
Harald Welteee3501f2021-04-02 13:00:18 +02001680 """Read a record and decode it to abstract data.
1681
1682 Args:
1683 rec_nr : Record number to read
1684 Returns:
1685 abstract data contained in record
1686 """
Harald Welteb2edd142021-01-08 23:29:35 +01001687 (data, sw) = self.read_record(rec_nr)
Harald Weltef6b37af2023-01-24 15:42:26 +01001688 return (self.selected_file.decode_record_hex(data, rec_nr), sw)
Harald Welteb2edd142021-01-08 23:29:35 +01001689
Harald Weltec91085e2022-02-10 18:05:45 +01001690 def update_record(self, rec_nr: int, data_hex: str):
Harald Welteee3501f2021-04-02 13:00:18 +02001691 """Update a record with given binary data
1692
1693 Args:
1694 rec_nr : Record number to read
1695 data_hex : Hex string binary data to be written
1696 """
Harald Welteb2edd142021-01-08 23:29:35 +01001697 if not isinstance(self.selected_file, LinFixedEF):
1698 raise TypeError("Only works with Linear Fixed EF")
Harald Weltea6c0f882022-07-17 14:23:17 +02001699 return self.rs.card._scc.update_record(self.selected_file.fid, rec_nr, data_hex, conserve=self.rs.conserve_write)
Harald Welteb2edd142021-01-08 23:29:35 +01001700
Harald Weltec91085e2022-02-10 18:05:45 +01001701 def update_record_dec(self, rec_nr: int, data: dict):
Harald Welteee3501f2021-04-02 13:00:18 +02001702 """Update a record with given abstract data. Will encode abstract to binary data
1703 and then write it to the given record on the card.
1704
1705 Args:
1706 rec_nr : Record number to read
1707 data_hex : Abstract data to be written
1708 """
Harald Weltef6b37af2023-01-24 15:42:26 +01001709 data_hex = self.selected_file.encode_record_hex(data, rec_nr)
Harald Welte1e456572021-04-02 17:16:30 +02001710 return self.update_record(rec_nr, data_hex)
Harald Welteb2edd142021-01-08 23:29:35 +01001711
Harald Weltec91085e2022-02-10 18:05:45 +01001712 def retrieve_data(self, tag: int = 0):
Harald Welte917d98c2021-04-21 11:51:25 +02001713 """Read a DO/TLV as binary data.
1714
1715 Args:
1716 tag : Tag of TLV/DO to read
1717 Returns:
1718 hex string of full BER-TLV DO including Tag and Length
1719 """
1720 if not isinstance(self.selected_file, BerTlvEF):
1721 raise TypeError("Only works with BER-TLV EF")
1722 # returns a string of hex nibbles
Harald Weltea6c0f882022-07-17 14:23:17 +02001723 return self.rs.card._scc.retrieve_data(self.selected_file.fid, tag)
Harald Welte917d98c2021-04-21 11:51:25 +02001724
1725 def retrieve_tags(self):
1726 """Retrieve tags available on BER-TLV EF.
1727
1728 Returns:
1729 list of integer tags contained in EF
1730 """
1731 if not isinstance(self.selected_file, BerTlvEF):
1732 raise TypeError("Only works with BER-TLV EF")
Harald Weltea6c0f882022-07-17 14:23:17 +02001733 data, sw = self.rs.card._scc.retrieve_data(self.selected_file.fid, 0x5c)
Harald Weltec1475302021-05-21 21:47:55 +02001734 tag, length, value, remainder = bertlv_parse_one(h2b(data))
Harald Welte917d98c2021-04-21 11:51:25 +02001735 return list(value)
1736
Harald Weltec91085e2022-02-10 18:05:45 +01001737 def set_data(self, tag: int, data_hex: str):
Harald Welte917d98c2021-04-21 11:51:25 +02001738 """Update a TLV/DO with given binary data
1739
1740 Args:
1741 tag : Tag of TLV/DO to be written
1742 data_hex : Hex string binary data to be written (value portion)
1743 """
1744 if not isinstance(self.selected_file, BerTlvEF):
1745 raise TypeError("Only works with BER-TLV EF")
Harald Weltea6c0f882022-07-17 14:23:17 +02001746 return self.rs.card._scc.set_data(self.selected_file.fid, tag, data_hex, conserve=self.rs.conserve_write)
Harald Welte917d98c2021-04-21 11:51:25 +02001747
Philipp Maier5d698e52021-09-16 13:18:01 +02001748 def unregister_cmds(self, cmd_app=None):
1749 """Unregister all file specific commands."""
1750 if cmd_app and self.selected_file.shell_commands:
1751 for c in self.selected_file.shell_commands:
1752 cmd_app.unregister_command_set(c)
Harald Welte917d98c2021-04-21 11:51:25 +02001753
Harald Welteb2edd142021-01-08 23:29:35 +01001754
Vadim Yanitskiy04b5d9d2022-07-07 03:05:30 +07001755class FileData:
Harald Welteb2edd142021-01-08 23:29:35 +01001756 """Represent the runtime, on-card data."""
Harald Weltec91085e2022-02-10 18:05:45 +01001757
Harald Welteb2edd142021-01-08 23:29:35 +01001758 def __init__(self, fdesc):
1759 self.desc = fdesc
1760 self.fcp = None
1761
1762
Harald Weltec91085e2022-02-10 18:05:45 +01001763def interpret_sw(sw_data: dict, sw: str):
Harald Welteee3501f2021-04-02 13:00:18 +02001764 """Interpret a given status word.
1765
1766 Args:
1767 sw_data : Hierarchical dict of status word matches
1768 sw : status word to match (string of 4 hex digits)
1769 Returns:
1770 tuple of two strings (class_string, description)
1771 """
Harald Welteb2edd142021-01-08 23:29:35 +01001772 for class_str, swdict in sw_data.items():
1773 # first try direct match
1774 if sw in swdict:
1775 return (class_str, swdict[sw])
1776 # next try wildcard matches
1777 for pattern, descr in swdict.items():
1778 if sw_match(sw, pattern):
1779 return (class_str, descr)
1780 return None
1781
Harald Weltec91085e2022-02-10 18:05:45 +01001782
Vadim Yanitskiy04b5d9d2022-07-07 03:05:30 +07001783class CardApplication:
Harald Welteb2edd142021-01-08 23:29:35 +01001784 """A card application is represented by an ADF (with contained hierarchy) and optionally
1785 some SW definitions."""
Harald Weltec91085e2022-02-10 18:05:45 +01001786
1787 def __init__(self, name, adf: Optional[CardADF] = None, aid: str = None, sw: dict = None):
Harald Welteee3501f2021-04-02 13:00:18 +02001788 """
1789 Args:
1790 adf : ADF name
1791 sw : Dict of status word conversions
1792 """
Harald Welteb2edd142021-01-08 23:29:35 +01001793 self.name = name
1794 self.adf = adf
Vadim Yanitskiy98f872b2021-03-27 01:25:46 +01001795 self.sw = sw or dict()
Harald Welte5ce35242021-04-02 20:27:05 +02001796 # back-reference from ADF to Applicaiton
1797 if self.adf:
1798 self.aid = aid or self.adf.aid
1799 self.adf.application = self
1800 else:
1801 self.aid = aid
Harald Welteb2edd142021-01-08 23:29:35 +01001802
1803 def __str__(self):
1804 return "APP(%s)" % (self.name)
1805
1806 def interpret_sw(self, sw):
Harald Welteee3501f2021-04-02 13:00:18 +02001807 """Interpret a given status word within the application.
1808
1809 Args:
Harald Weltec9cdce32021-04-11 10:28:28 +02001810 sw : Status word as string of 4 hex digits
Harald Welteee3501f2021-04-02 13:00:18 +02001811
1812 Returns:
1813 Tuple of two strings
1814 """
Harald Welteb2edd142021-01-08 23:29:35 +01001815 return interpret_sw(self.sw, sw)
1816
Harald Weltef44256c2021-10-14 15:53:39 +02001817
1818class CardModel(abc.ABC):
Harald Welte4c1dca02021-10-14 17:48:25 +02001819 """A specific card model, typically having some additional vendor-specific files. All
1820 you need to do is to define a sub-class with a list of ATRs or an overridden match
1821 method."""
Harald Weltef44256c2021-10-14 15:53:39 +02001822 _atrs = []
1823
1824 @classmethod
1825 @abc.abstractmethod
Harald Weltec91085e2022-02-10 18:05:45 +01001826 def add_files(cls, rs: RuntimeState):
Harald Weltef44256c2021-10-14 15:53:39 +02001827 """Add model specific files to given RuntimeState."""
1828
1829 @classmethod
Harald Weltec91085e2022-02-10 18:05:45 +01001830 def match(cls, scc: SimCardCommands) -> bool:
Harald Weltef44256c2021-10-14 15:53:39 +02001831 """Test if given card matches this model."""
1832 card_atr = scc.get_atr()
1833 for atr in cls._atrs:
1834 atr_bin = toBytes(atr)
1835 if atr_bin == card_atr:
1836 print("Detected CardModel:", cls.__name__)
1837 return True
1838 return False
1839
1840 @staticmethod
Harald Weltec91085e2022-02-10 18:05:45 +01001841 def apply_matching_models(scc: SimCardCommands, rs: RuntimeState):
Harald Welte4c1dca02021-10-14 17:48:25 +02001842 """Check if any of the CardModel sub-classes 'match' the currently inserted card
1843 (by ATR or overriding the 'match' method). If so, call their 'add_files'
1844 method."""
Harald Weltef44256c2021-10-14 15:53:39 +02001845 for m in CardModel.__subclasses__():
1846 if m.match(scc):
1847 m.add_files(rs)