blob: 5950ad14151094639af0608a2d459d9b9dee00bf [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
Vadim Yanitskiy04b5d9d2022-07-07 03:05:30 +070054class CardFile:
Harald Welteb2edd142021-01-08 23:29:35 +010055 """Base class for all objects in the smart card filesystem.
56 Serve as a common ancestor to all other file types; rarely used directly.
57 """
58 RESERVED_NAMES = ['..', '.', '/', 'MF']
59 RESERVED_FIDS = ['3f00']
60
Harald Weltec91085e2022-02-10 18:05:45 +010061 def __init__(self, fid: str = None, sfid: str = None, name: str = None, desc: str = None,
Harald Welte9170fbf2022-02-11 21:54:37 +010062 parent: Optional['CardDF'] = None, profile: Optional['CardProfile'] = None,
63 service: Optional[CardFileService] = None):
Harald Welteee3501f2021-04-02 13:00:18 +020064 """
65 Args:
66 fid : File Identifier (4 hex digits)
67 sfid : Short File Identifier (2 hex digits, optional)
68 name : Brief name of the file, lik EF_ICCID
Harald Weltec9cdce32021-04-11 10:28:28 +020069 desc : Description of the file
Harald Welteee3501f2021-04-02 13:00:18 +020070 parent : Parent CardFile object within filesystem hierarchy
Philipp Maier5af7bdf2021-11-04 12:48:41 +010071 profile : Card profile that this file should be part of
Harald Welte9170fbf2022-02-11 21:54:37 +010072 service : Service (SST/UST/IST) associated with the file
Harald Welteee3501f2021-04-02 13:00:18 +020073 """
Harald Welteb2edd142021-01-08 23:29:35 +010074 if not isinstance(self, CardADF) and fid == None:
75 raise ValueError("fid is mandatory")
76 if fid:
77 fid = fid.lower()
78 self.fid = fid # file identifier
79 self.sfid = sfid # short file identifier
80 self.name = name # human readable name
81 self.desc = desc # human readable description
82 self.parent = parent
83 if self.parent and self.parent != self and self.fid:
84 self.parent.add_file(self)
Philipp Maier5af7bdf2021-11-04 12:48:41 +010085 self.profile = profile
Harald Welte9170fbf2022-02-11 21:54:37 +010086 self.service = service
Harald Weltec91085e2022-02-10 18:05:45 +010087 self.shell_commands = [] # type: List[CommandSet]
Harald Welteb2edd142021-01-08 23:29:35 +010088
Harald Weltec91085e2022-02-10 18:05:45 +010089 # Note: the basic properties (fid, name, ect.) are verified when
90 # the file is attached to a parent file. See method add_file() in
91 # class Card DF
Philipp Maier66061582021-03-09 21:57:57 +010092
Harald Welteb2edd142021-01-08 23:29:35 +010093 def __str__(self):
94 if self.name:
95 return self.name
96 else:
97 return self.fid
98
Harald Weltec91085e2022-02-10 18:05:45 +010099 def _path_element(self, prefer_name: bool) -> Optional[str]:
Harald Welteb2edd142021-01-08 23:29:35 +0100100 if prefer_name and self.name:
101 return self.name
102 else:
103 return self.fid
104
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200105 def fully_qualified_path_str(self, prefer_name: bool = True) -> str:
106 """Return fully qualified path to file as string.
107
108 Args:
109 prefer_name : Preferably build path of names; fall-back to FIDs as required
110 """
111 return '/'.join(self.fully_qualified_path(prefer_name))
112
Harald Weltec91085e2022-02-10 18:05:45 +0100113 def fully_qualified_path(self, prefer_name: bool = True) -> List[str]:
Harald Welteee3501f2021-04-02 13:00:18 +0200114 """Return fully qualified path to file as list of FID or name strings.
115
116 Args:
117 prefer_name : Preferably build path of names; fall-back to FIDs as required
118 """
Harald Welte1e456572021-04-02 17:16:30 +0200119 if self.parent and self.parent != self:
Harald Welteb2edd142021-01-08 23:29:35 +0100120 ret = self.parent.fully_qualified_path(prefer_name)
121 else:
122 ret = []
Harald Welte1e456572021-04-02 17:16:30 +0200123 elem = self._path_element(prefer_name)
124 if elem:
125 ret.append(elem)
Harald Welteb2edd142021-01-08 23:29:35 +0100126 return ret
127
Harald Welteaceb2a52022-02-12 21:41:59 +0100128 def fully_qualified_path_fobj(self) -> List['CardFile']:
129 """Return fully qualified path to file as list of CardFile instance references."""
130 if self.parent and self.parent != self:
131 ret = self.parent.fully_qualified_path_fobj()
132 else:
133 ret = []
134 if self:
135 ret.append(self)
136 return ret
137
138 def build_select_path_to(self, target: 'CardFile') -> Optional[List['CardFile']]:
139 """Build the relative sequence of files we need to traverse to get from us to 'target'."""
Harald Welte02a7f742023-07-11 09:21:52 +0200140 # special-case handling for selecting MF while we MF is selected
141 if target == target.get_mf():
142 return [target]
Harald Welteaceb2a52022-02-12 21:41:59 +0100143 cur_fqpath = self.fully_qualified_path_fobj()
144 target_fqpath = target.fully_qualified_path_fobj()
145 inter_path = []
146 cur_fqpath.pop() # drop last element (currently selected file, doesn't need re-selection
147 cur_fqpath.reverse()
148 for ce in cur_fqpath:
149 inter_path.append(ce)
150 for i in range(0, len(target_fqpath)-1):
151 te = target_fqpath[i]
152 if te == ce:
153 for te2 in target_fqpath[i+1:]:
154 inter_path.append(te2)
155 # we found our common ancestor
156 return inter_path
157 return None
158
Harald Welteee3501f2021-04-02 13:00:18 +0200159 def get_mf(self) -> Optional['CardMF']:
Harald Welteb2edd142021-01-08 23:29:35 +0100160 """Return the MF (root) of the file system."""
161 if self.parent == None:
162 return None
163 # iterate towards the top. MF has parent == self
164 node = self
Harald Welte1e456572021-04-02 17:16:30 +0200165 while node.parent and node.parent != node:
Harald Welteb2edd142021-01-08 23:29:35 +0100166 node = node.parent
Harald Welte1e456572021-04-02 17:16:30 +0200167 return cast(CardMF, node)
Harald Welteb2edd142021-01-08 23:29:35 +0100168
Harald Weltec91085e2022-02-10 18:05:45 +0100169 def _get_self_selectables(self, alias: str = None, flags=[]) -> Dict[str, 'CardFile']:
Harald Welteee3501f2021-04-02 13:00:18 +0200170 """Return a dict of {'identifier': self} tuples.
171
172 Args:
173 alias : Add an alias with given name to 'self'
174 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
175 If not specified, all selectables will be returned.
176 Returns:
177 dict containing reference to 'self' for all identifiers.
178 """
Harald Welteb2edd142021-01-08 23:29:35 +0100179 sels = {}
180 if alias:
181 sels.update({alias: self})
Philipp Maier786f7812021-02-25 16:48:10 +0100182 if self.fid and (flags == [] or 'FIDS' in flags):
Harald Welteb2edd142021-01-08 23:29:35 +0100183 sels.update({self.fid: self})
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100184 if self.name and (flags == [] or 'FNAMES' in flags):
Harald Welteb2edd142021-01-08 23:29:35 +0100185 sels.update({self.name: self})
186 return sels
187
Harald Weltef5ff1b82022-07-24 12:19:57 +0200188 def _get_parent_selectables(self, alias: Optional[str] = None, flags=[]) -> Dict[str, 'CardFile']:
189 sels = {}
190 if not self.parent or self.parent == self:
191 return sels
192 # add our immediate parent
193 if alias:
194 sels.update({alias: self.parent})
195 if self.parent.fid and (flags == [] or 'FIDS' in flags):
196 sels.update({self.parent.fid: self.parent})
197 if self.parent.name and (flags == [] or 'FNAMES' in flags):
198 sels.update({self.parent.name: self.parent})
199 # recurse to parents of our parent, but without any alias
200 sels.update(self.parent._get_parent_selectables(None, flags))
201 return sels
202
Harald Weltec91085e2022-02-10 18:05:45 +0100203 def get_selectables(self, flags=[]) -> Dict[str, 'CardFile']:
Harald Welteee3501f2021-04-02 13:00:18 +0200204 """Return a dict of {'identifier': File} that is selectable from the current file.
205
206 Args:
207 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
208 If not specified, all selectables will be returned.
209 Returns:
210 dict containing all selectable items. Key is identifier (string), value
211 a reference to a CardFile (or derived class) instance.
212 """
Philipp Maier786f7812021-02-25 16:48:10 +0100213 sels = {}
Harald Welteb2edd142021-01-08 23:29:35 +0100214 # we can always select ourself
Philipp Maier786f7812021-02-25 16:48:10 +0100215 if flags == [] or 'SELF' in flags:
216 sels = self._get_self_selectables('.', flags)
Harald Welteb2edd142021-01-08 23:29:35 +0100217 # we can always select our parent
Philipp Maier786f7812021-02-25 16:48:10 +0100218 if flags == [] or 'PARENT' in flags:
Harald Weltef5ff1b82022-07-24 12:19:57 +0200219 sels.update(self._get_parent_selectables('..', flags))
Harald Welteb2edd142021-01-08 23:29:35 +0100220 # if we have a MF, we can always select its applications
Philipp Maier786f7812021-02-25 16:48:10 +0100221 if flags == [] or 'MF' in flags:
222 mf = self.get_mf()
223 if mf:
Harald Weltec91085e2022-02-10 18:05:45 +0100224 sels.update(mf._get_self_selectables(flags=flags))
225 sels.update(mf.get_app_selectables(flags=flags))
Harald Welteb2edd142021-01-08 23:29:35 +0100226 return sels
227
Harald Weltec91085e2022-02-10 18:05:45 +0100228 def get_selectable_names(self, flags=[]) -> List[str]:
Harald Welteee3501f2021-04-02 13:00:18 +0200229 """Return a dict of {'identifier': File} that is selectable from the current file.
230
231 Args:
232 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
233 If not specified, all selectables will be returned.
234 Returns:
Harald Welte1e456572021-04-02 17:16:30 +0200235 list containing all selectable names.
Harald Welteee3501f2021-04-02 13:00:18 +0200236 """
Philipp Maier786f7812021-02-25 16:48:10 +0100237 sels = self.get_selectables(flags)
Harald Welteb3d68c02022-01-21 15:31:29 +0100238 sel_keys = list(sels.keys())
239 sel_keys.sort()
240 return sel_keys
Harald Welteb2edd142021-01-08 23:29:35 +0100241
Harald Weltec91085e2022-02-10 18:05:45 +0100242 def decode_select_response(self, data_hex: str):
Philipp Maier5af7bdf2021-11-04 12:48:41 +0100243 """Decode the response to a SELECT command.
244
245 Args:
Harald Weltec91085e2022-02-10 18:05:45 +0100246 data_hex: Hex string of the select response
247 """
Philipp Maier5af7bdf2021-11-04 12:48:41 +0100248
Harald Weltec91085e2022-02-10 18:05:45 +0100249 # When the current file does not implement a custom select response decoder,
250 # we just ask the parent file to decode the select response. If this method
251 # is not overloaded by the current file we will again ask the parent file.
252 # This way we recursively travel up the file system tree until we hit a file
253 # that does implement a concrete decoder.
Harald Welte1e456572021-04-02 17:16:30 +0200254 if self.parent:
255 return self.parent.decode_select_response(data_hex)
Harald Welteb2edd142021-01-08 23:29:35 +0100256
Philipp Maier5af7bdf2021-11-04 12:48:41 +0100257 def get_profile(self):
258 """Get the profile associated with this file. If this file does not have any
259 profile assigned, try to find a file above (usually the MF) in the filesystem
260 hirarchy that has a profile assigned
261 """
262
263 # If we have a profile set, return it
264 if self.profile:
265 return self.profile
266
267 # Walk up recursively until we hit a parent that has a profile set
268 if self.parent:
269 return self.parent.get_profile()
270 return None
Harald Welteb2edd142021-01-08 23:29:35 +0100271
Harald Welte9170fbf2022-02-11 21:54:37 +0100272 def should_exist_for_services(self, services: List[int]):
273 """Assuming the provided list of activated services, should this file exist and be activated?."""
274 if self.service is None:
275 return None
276 elif isinstance(self.service, int):
277 # a single service determines the result
278 return self.service in services
279 elif isinstance(self.service, list):
280 # any of the services active -> true
281 for s in self.service:
282 if s in services:
283 return True
284 return False
285 elif isinstance(self.service, tuple):
286 # all of the services active -> true
287 for s in self.service:
288 if not s in services:
289 return False
290 return True
291 else:
292 raise ValueError("self.service must be either int or list or tuple")
293
Harald Weltec91085e2022-02-10 18:05:45 +0100294
Harald Welteb2edd142021-01-08 23:29:35 +0100295class CardDF(CardFile):
296 """DF (Dedicated File) in the smart card filesystem. Those are basically sub-directories."""
Philipp Maier63f572d2021-03-09 22:42:47 +0100297
298 @with_default_category('DF/ADF Commands')
299 class ShellCommands(CommandSet):
300 def __init__(self):
301 super().__init__()
302
Harald Welteb2edd142021-01-08 23:29:35 +0100303 def __init__(self, **kwargs):
304 if not isinstance(self, CardADF):
305 if not 'fid' in kwargs:
306 raise TypeError('fid is mandatory for all DF')
307 super().__init__(**kwargs)
308 self.children = dict()
Philipp Maier63f572d2021-03-09 22:42:47 +0100309 self.shell_commands = [self.ShellCommands()]
Harald Welte9170fbf2022-02-11 21:54:37 +0100310 # dict of CardFile affected by service(int), indexed by service
311 self.files_by_service = {}
Harald Welteb2edd142021-01-08 23:29:35 +0100312
313 def __str__(self):
314 return "DF(%s)" % (super().__str__())
315
Harald Welte9170fbf2022-02-11 21:54:37 +0100316 def _add_file_services(self, child):
317 """Add a child (DF/EF) to the files_by_services of the parent."""
318 if not child.service:
319 return
320 if isinstance(child.service, int):
321 self.files_by_service.setdefault(child.service, []).append(child)
322 elif isinstance(child.service, list):
323 for service in child.service:
324 self.files_by_service.setdefault(service, []).append(child)
325 elif isinstance(child.service, tuple):
326 for service in child.service:
327 self.files_by_service.setdefault(service, []).append(child)
328 else:
329 raise ValueError
330
Harald Welted56f45d2022-07-16 11:46:59 +0200331 def _has_service(self):
332 if self.service:
333 return True
334 for c in self.children.values():
335 if isinstance(c, CardDF):
336 if c._has_service():
337 return True
338
Harald Weltec91085e2022-02-10 18:05:45 +0100339 def add_file(self, child: CardFile, ignore_existing: bool = False):
Harald Welteee3501f2021-04-02 13:00:18 +0200340 """Add a child (DF/EF) to this DF.
341 Args:
342 child: The new DF/EF to be added
343 ignore_existing: Ignore, if file with given FID already exists. Old one will be kept.
344 """
Harald Welteb2edd142021-01-08 23:29:35 +0100345 if not isinstance(child, CardFile):
346 raise TypeError("Expected a File instance")
Harald Weltec91085e2022-02-10 18:05:45 +0100347 if not is_hex(child.fid, minlen=4, maxlen=4):
Philipp Maier3aec8712021-03-09 21:49:01 +0100348 raise ValueError("File name %s is not a valid fid" % (child.fid))
Harald Welteb2edd142021-01-08 23:29:35 +0100349 if child.name in CardFile.RESERVED_NAMES:
350 raise ValueError("File name %s is a reserved name" % (child.name))
351 if child.fid in CardFile.RESERVED_FIDS:
Philipp Maiere8bc1b42021-03-09 20:33:41 +0100352 raise ValueError("File fid %s is a reserved fid" % (child.fid))
Harald Welteb2edd142021-01-08 23:29:35 +0100353 if child.fid in self.children:
354 if ignore_existing:
355 return
Harald Weltec91085e2022-02-10 18:05:45 +0100356 raise ValueError(
357 "File with given fid %s already exists in %s" % (child.fid, self))
Harald Welteb2edd142021-01-08 23:29:35 +0100358 if self.lookup_file_by_sfid(child.sfid):
Harald Weltec91085e2022-02-10 18:05:45 +0100359 raise ValueError(
360 "File with given sfid %s already exists in %s" % (child.sfid, self))
Harald Welteb2edd142021-01-08 23:29:35 +0100361 if self.lookup_file_by_name(child.name):
362 if ignore_existing:
363 return
Harald Weltec91085e2022-02-10 18:05:45 +0100364 raise ValueError(
365 "File with given name %s already exists in %s" % (child.name, self))
Harald Welteb2edd142021-01-08 23:29:35 +0100366 self.children[child.fid] = child
367 child.parent = self
Harald Welte419bb492022-02-12 21:39:35 +0100368 # update the service -> file relationship table
Harald Welte9170fbf2022-02-11 21:54:37 +0100369 self._add_file_services(child)
Harald Welte419bb492022-02-12 21:39:35 +0100370 if isinstance(child, CardDF):
371 for c in child.children.values():
372 self._add_file_services(c)
373 if isinstance(c, CardDF):
Harald Welted56f45d2022-07-16 11:46:59 +0200374 for gc in c.children.values():
375 if isinstance(gc, CardDF):
376 if gc._has_service():
377 raise ValueError('TODO: implement recursive service -> file mapping')
Harald Welteb2edd142021-01-08 23:29:35 +0100378
Harald Weltec91085e2022-02-10 18:05:45 +0100379 def add_files(self, children: Iterable[CardFile], ignore_existing: bool = False):
Harald Welteee3501f2021-04-02 13:00:18 +0200380 """Add a list of child (DF/EF) to this DF
381
382 Args:
383 children: List of new DF/EFs to be added
384 ignore_existing: Ignore, if file[s] with given FID already exists. Old one[s] will be kept.
385 """
Harald Welteb2edd142021-01-08 23:29:35 +0100386 for child in children:
387 self.add_file(child, ignore_existing)
388
Harald Weltec91085e2022-02-10 18:05:45 +0100389 def get_selectables(self, flags=[]) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +0200390 """Return a dict of {'identifier': File} that is selectable from the current DF.
391
392 Args:
393 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
394 If not specified, all selectables will be returned.
395 Returns:
396 dict containing all selectable items. Key is identifier (string), value
397 a reference to a CardFile (or derived class) instance.
398 """
Harald Welteb2edd142021-01-08 23:29:35 +0100399 # global selectables + our children
Philipp Maier786f7812021-02-25 16:48:10 +0100400 sels = super().get_selectables(flags)
401 if flags == [] or 'FIDS' in flags:
Harald Weltec91085e2022-02-10 18:05:45 +0100402 sels.update({x.fid: x for x in self.children.values() if x.fid})
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100403 if flags == [] or 'FNAMES' in flags:
Harald Weltec91085e2022-02-10 18:05:45 +0100404 sels.update({x.name: x for x in self.children.values() if x.name})
Harald Welteb2edd142021-01-08 23:29:35 +0100405 return sels
406
Harald Weltec91085e2022-02-10 18:05:45 +0100407 def lookup_file_by_name(self, name: Optional[str]) -> Optional[CardFile]:
Harald Welteee3501f2021-04-02 13:00:18 +0200408 """Find a file with given name within current DF."""
Harald Welteb2edd142021-01-08 23:29:35 +0100409 if name == None:
410 return None
411 for i in self.children.values():
412 if i.name and i.name == name:
413 return i
414 return None
415
Harald Weltec91085e2022-02-10 18:05:45 +0100416 def lookup_file_by_sfid(self, sfid: Optional[str]) -> Optional[CardFile]:
Harald Welteee3501f2021-04-02 13:00:18 +0200417 """Find a file with given short file ID within current DF."""
Harald Welteb2edd142021-01-08 23:29:35 +0100418 if sfid == None:
419 return None
420 for i in self.children.values():
Harald Welte1e456572021-04-02 17:16:30 +0200421 if i.sfid == int(str(sfid)):
Harald Welteb2edd142021-01-08 23:29:35 +0100422 return i
423 return None
424
Harald Weltec91085e2022-02-10 18:05:45 +0100425 def lookup_file_by_fid(self, fid: str) -> Optional[CardFile]:
Harald Welteee3501f2021-04-02 13:00:18 +0200426 """Find a file with given file ID within current DF."""
Harald Welteb2edd142021-01-08 23:29:35 +0100427 if fid in self.children:
428 return self.children[fid]
429 return None
430
431
432class CardMF(CardDF):
433 """MF (Master File) in the smart card filesystem"""
Harald Weltec91085e2022-02-10 18:05:45 +0100434
Harald Welteb2edd142021-01-08 23:29:35 +0100435 def __init__(self, **kwargs):
436 # can be overridden; use setdefault
437 kwargs.setdefault('fid', '3f00')
438 kwargs.setdefault('name', 'MF')
439 kwargs.setdefault('desc', 'Master File (directory root)')
440 # cannot be overridden; use assignment
441 kwargs['parent'] = self
442 super().__init__(**kwargs)
443 self.applications = dict()
444
445 def __str__(self):
446 return "MF(%s)" % (self.fid)
447
Harald Weltec91085e2022-02-10 18:05:45 +0100448 def add_application_df(self, app: 'CardADF'):
Harald Welte5ce35242021-04-02 20:27:05 +0200449 """Add an Application to the MF"""
Harald Welteb2edd142021-01-08 23:29:35 +0100450 if not isinstance(app, CardADF):
451 raise TypeError("Expected an ADF instance")
452 if app.aid in self.applications:
453 raise ValueError("AID %s already exists" % (app.aid))
454 self.applications[app.aid] = app
Harald Weltec91085e2022-02-10 18:05:45 +0100455 app.parent = self
Harald Welteb2edd142021-01-08 23:29:35 +0100456
457 def get_app_names(self):
458 """Get list of completions (AID names)"""
Harald Welted53918c2022-02-12 18:20:49 +0100459 return list(self.applications.values())
Harald Welteb2edd142021-01-08 23:29:35 +0100460
Harald Weltec91085e2022-02-10 18:05:45 +0100461 def get_selectables(self, flags=[]) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +0200462 """Return a dict of {'identifier': File} that is selectable from the current DF.
463
464 Args:
465 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
466 If not specified, all selectables will be returned.
467 Returns:
468 dict containing all selectable items. Key is identifier (string), value
469 a reference to a CardFile (or derived class) instance.
470 """
Philipp Maier786f7812021-02-25 16:48:10 +0100471 sels = super().get_selectables(flags)
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100472 sels.update(self.get_app_selectables(flags))
Harald Welteb2edd142021-01-08 23:29:35 +0100473 return sels
474
Harald Weltec91085e2022-02-10 18:05:45 +0100475 def get_app_selectables(self, flags=[]) -> dict:
Philipp Maier786f7812021-02-25 16:48:10 +0100476 """Get applications by AID + name"""
477 sels = {}
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100478 if flags == [] or 'AIDS' in flags:
Harald Weltec91085e2022-02-10 18:05:45 +0100479 sels.update({x.aid: x for x in self.applications.values()})
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100480 if flags == [] or 'ANAMES' in flags:
Harald Weltec91085e2022-02-10 18:05:45 +0100481 sels.update(
482 {x.name: x for x in self.applications.values() if x.name})
Harald Welteb2edd142021-01-08 23:29:35 +0100483 return sels
484
Harald Weltec9752512022-02-11 16:31:15 +0100485 def decode_select_response(self, data_hex: Optional[str]) -> object:
Harald Welteee3501f2021-04-02 13:00:18 +0200486 """Decode the response to a SELECT command.
487
Philipp Maier5af7bdf2021-11-04 12:48:41 +0100488 This is the fall-back method which automatically defers to the standard decoding
489 method defined by the card profile. When no profile is set, then no decoding is
Harald Weltec91085e2022-02-10 18:05:45 +0100490 performed. Specific derived classes (usually ADF) can overload this method to
491 install specific decoding.
Harald Welteee3501f2021-04-02 13:00:18 +0200492 """
Harald Welteb2edd142021-01-08 23:29:35 +0100493
Harald Weltec9752512022-02-11 16:31:15 +0100494 if not data_hex:
495 return data_hex
496
Philipp Maier5af7bdf2021-11-04 12:48:41 +0100497 profile = self.get_profile()
Harald Welteb2edd142021-01-08 23:29:35 +0100498
Philipp Maier5af7bdf2021-11-04 12:48:41 +0100499 if profile:
500 return profile.decode_select_response(data_hex)
501 else:
502 return data_hex
Harald Welteb2edd142021-01-08 23:29:35 +0100503
Harald Weltec91085e2022-02-10 18:05:45 +0100504
Harald Welteb2edd142021-01-08 23:29:35 +0100505class CardADF(CardDF):
506 """ADF (Application Dedicated File) in the smart card filesystem"""
Harald Weltec91085e2022-02-10 18:05:45 +0100507
508 def __init__(self, aid: str, **kwargs):
Harald Welteb2edd142021-01-08 23:29:35 +0100509 super().__init__(**kwargs)
Harald Welte5ce35242021-04-02 20:27:05 +0200510 # reference to CardApplication may be set from CardApplication constructor
Harald Weltefe8a7442021-04-10 11:51:54 +0200511 self.application = None # type: Optional[CardApplication]
Harald Welteb2edd142021-01-08 23:29:35 +0100512 self.aid = aid # Application Identifier
Harald Welte1e456572021-04-02 17:16:30 +0200513 mf = self.get_mf()
514 if mf:
Harald Welte5ce35242021-04-02 20:27:05 +0200515 mf.add_application_df(self)
Harald Welteb2edd142021-01-08 23:29:35 +0100516
517 def __str__(self):
Harald Welte4b003652022-07-16 11:55:07 +0200518 return "ADF(%s)" % (self.name if self.name else self.aid)
Harald Welteb2edd142021-01-08 23:29:35 +0100519
Harald Weltec91085e2022-02-10 18:05:45 +0100520 def _path_element(self, prefer_name: bool):
Harald Welteb2edd142021-01-08 23:29:35 +0100521 if self.name and prefer_name:
522 return self.name
523 else:
524 return self.aid
525
526
527class CardEF(CardFile):
528 """EF (Entry File) in the smart card filesystem"""
Harald Weltec91085e2022-02-10 18:05:45 +0100529
Harald Welteb2edd142021-01-08 23:29:35 +0100530 def __init__(self, *, fid, **kwargs):
531 kwargs['fid'] = fid
532 super().__init__(**kwargs)
533
534 def __str__(self):
535 return "EF(%s)" % (super().__str__())
536
Harald Weltec91085e2022-02-10 18:05:45 +0100537 def get_selectables(self, flags=[]) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +0200538 """Return a dict of {'identifier': File} that is selectable from the current DF.
539
540 Args:
541 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
542 If not specified, all selectables will be returned.
543 Returns:
544 dict containing all selectable items. Key is identifier (string), value
545 a reference to a CardFile (or derived class) instance.
546 """
Harald Weltec91085e2022-02-10 18:05:45 +0100547 # global selectable names + those of the parent DF
Philipp Maier786f7812021-02-25 16:48:10 +0100548 sels = super().get_selectables(flags)
Harald Welted2c177b2022-07-24 11:35:53 +0200549 if flags == [] or 'FIDS' in flags:
550 sels.update({x.fid: x for x in self.parent.children.values() if x.fid and x != self})
551 if flags == [] or 'FNAMES' in flags:
552 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 +0100553 return sels
554
555
556class TransparentEF(CardEF):
Harald Welteee3501f2021-04-02 13:00:18 +0200557 """Transparent EF (Entry File) in the smart card filesystem.
558
559 A Transparent EF is a binary file with no formal structure. This is contrary to
560 Record based EFs which have [fixed size] records that can be individually read/updated."""
Harald Welteb2edd142021-01-08 23:29:35 +0100561
562 @with_default_category('Transparent EF Commands')
563 class ShellCommands(CommandSet):
Harald Weltec9cdce32021-04-11 10:28:28 +0200564 """Shell commands specific for transparent EFs."""
Harald Weltec91085e2022-02-10 18:05:45 +0100565
Harald Welteb2edd142021-01-08 23:29:35 +0100566 def __init__(self):
567 super().__init__()
568
Harald Welteaefd0642022-02-25 15:26:37 +0100569 dec_hex_parser = argparse.ArgumentParser()
570 dec_hex_parser.add_argument('--oneline', action='store_true',
571 help='No JSON pretty-printing, dump as a single line')
572 dec_hex_parser.add_argument('HEXSTR', help='Hex-string of encoded data to decode')
573
574 @cmd2.with_argparser(dec_hex_parser)
575 def do_decode_hex(self, opts):
576 """Decode command-line provided hex-string as if it was read from the file."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200577 data = self._cmd.lchan.selected_file.decode_hex(opts.HEXSTR)
Harald Welteaefd0642022-02-25 15:26:37 +0100578 self._cmd.poutput_json(data, opts.oneline)
579
Harald Welteb2edd142021-01-08 23:29:35 +0100580 read_bin_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100581 read_bin_parser.add_argument(
582 '--offset', type=int, default=0, help='Byte offset for start of read')
583 read_bin_parser.add_argument(
584 '--length', type=int, help='Number of bytes to read')
585
Harald Welteb2edd142021-01-08 23:29:35 +0100586 @cmd2.with_argparser(read_bin_parser)
587 def do_read_binary(self, opts):
588 """Read binary data from a transparent EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200589 (data, sw) = self._cmd.lchan.read_binary(opts.length, opts.offset)
Harald Welteb2edd142021-01-08 23:29:35 +0100590 self._cmd.poutput(data)
591
Harald Weltebcad86c2021-04-06 20:08:39 +0200592 read_bin_dec_parser = argparse.ArgumentParser()
593 read_bin_dec_parser.add_argument('--oneline', action='store_true',
594 help='No JSON pretty-printing, dump as a single line')
Harald Weltec91085e2022-02-10 18:05:45 +0100595
Harald Weltebcad86c2021-04-06 20:08:39 +0200596 @cmd2.with_argparser(read_bin_dec_parser)
Harald Welteb2edd142021-01-08 23:29:35 +0100597 def do_read_binary_decoded(self, opts):
598 """Read + decode data from a transparent EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200599 (data, sw) = self._cmd.lchan.read_binary_dec()
Harald Welte1748b932021-04-06 21:12:25 +0200600 self._cmd.poutput_json(data, opts.oneline)
Harald Welteb2edd142021-01-08 23:29:35 +0100601
602 upd_bin_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100603 upd_bin_parser.add_argument(
604 '--offset', type=int, default=0, help='Byte offset for start of read')
605 upd_bin_parser.add_argument(
606 'data', help='Data bytes (hex format) to write')
607
Harald Welteb2edd142021-01-08 23:29:35 +0100608 @cmd2.with_argparser(upd_bin_parser)
609 def do_update_binary(self, opts):
610 """Update (Write) data of a transparent EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200611 (data, sw) = self._cmd.lchan.update_binary(opts.data, opts.offset)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100612 if data:
613 self._cmd.poutput(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100614
615 upd_bin_dec_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100616 upd_bin_dec_parser.add_argument(
617 'data', help='Abstract data (JSON format) to write')
Harald Welte0d4e98a2021-04-07 00:14:40 +0200618 upd_bin_dec_parser.add_argument('--json-path', type=str,
619 help='JSON path to modify specific element of file only')
Harald Weltec91085e2022-02-10 18:05:45 +0100620
Harald Welteb2edd142021-01-08 23:29:35 +0100621 @cmd2.with_argparser(upd_bin_dec_parser)
622 def do_update_binary_decoded(self, opts):
623 """Encode + Update (Write) data of a transparent EF"""
Harald Welte0d4e98a2021-04-07 00:14:40 +0200624 if opts.json_path:
Harald Weltea6c0f882022-07-17 14:23:17 +0200625 (data_json, sw) = self._cmd.lchan.read_binary_dec()
Harald Weltec91085e2022-02-10 18:05:45 +0100626 js_path_modify(data_json, opts.json_path,
627 json.loads(opts.data))
Harald Welte0d4e98a2021-04-07 00:14:40 +0200628 else:
629 data_json = json.loads(opts.data)
Harald Weltea6c0f882022-07-17 14:23:17 +0200630 (data, sw) = self._cmd.lchan.update_binary_dec(data_json)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100631 if data:
Harald Welte1748b932021-04-06 21:12:25 +0200632 self._cmd.poutput_json(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100633
Harald Welte4145d3c2021-04-08 20:34:13 +0200634 def do_edit_binary_decoded(self, opts):
635 """Edit the JSON representation of the EF contents in an editor."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200636 (orig_json, sw) = self._cmd.lchan.read_binary_dec()
Harald Welte4145d3c2021-04-08 20:34:13 +0200637 with tempfile.TemporaryDirectory(prefix='pysim_') as dirname:
638 filename = '%s/file' % dirname
639 # write existing data as JSON to file
640 with open(filename, 'w') as text_file:
641 json.dump(orig_json, text_file, indent=4)
642 # run a text editor
Harald Weltee1268722023-06-24 07:57:08 +0200643 self._cmd.run_editor(filename)
Harald Welte4145d3c2021-04-08 20:34:13 +0200644 with open(filename, 'r') as text_file:
645 edited_json = json.load(text_file)
646 if edited_json == orig_json:
647 self._cmd.poutput("Data not modified, skipping write")
648 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200649 (data, sw) = self._cmd.lchan.update_binary_dec(edited_json)
Harald Welte4145d3c2021-04-08 20:34:13 +0200650 if data:
651 self._cmd.poutput_json(data)
652
Harald Weltec91085e2022-02-10 18:05:45 +0100653 def __init__(self, fid: str, sfid: str = None, name: str = None, desc: str = None, parent: CardDF = None,
Harald Welte13edf302022-07-21 15:19:23 +0200654 size: Size = (1, None), **kwargs):
Harald Welteee3501f2021-04-02 13:00:18 +0200655 """
656 Args:
657 fid : File Identifier (4 hex digits)
658 sfid : Short File Identifier (2 hex digits, optional)
659 name : Brief name of the file, lik EF_ICCID
Harald Weltec9cdce32021-04-11 10:28:28 +0200660 desc : Description of the file
Harald Welteee3501f2021-04-02 13:00:18 +0200661 parent : Parent CardFile object within filesystem hierarchy
662 size : tuple of (minimum_size, recommended_size)
663 """
Harald Welte9170fbf2022-02-11 21:54:37 +0100664 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, **kwargs)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200665 self._construct = None
Harald Weltefb506212021-05-29 21:28:24 +0200666 self._tlv = None
Harald Welteb2edd142021-01-08 23:29:35 +0100667 self.size = size
668 self.shell_commands = [self.ShellCommands()]
669
Harald Weltec91085e2022-02-10 18:05:45 +0100670 def decode_bin(self, raw_bin_data: bytearray) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +0200671 """Decode raw (binary) data into abstract representation.
672
673 A derived class would typically provide a _decode_bin() or _decode_hex() method
674 for implementing this specifically for the given file. This function checks which
675 of the method exists, add calls them (with conversion, as needed).
676
677 Args:
678 raw_bin_data : binary encoded data
679 Returns:
680 abstract_data; dict representing the decoded data
681 """
Harald Welteb2edd142021-01-08 23:29:35 +0100682 method = getattr(self, '_decode_bin', None)
683 if callable(method):
684 return method(raw_bin_data)
685 method = getattr(self, '_decode_hex', None)
686 if callable(method):
687 return method(b2h(raw_bin_data))
Harald Welte2db5cfb2021-04-10 19:05:37 +0200688 if self._construct:
Harald Welte07c7b1f2021-05-28 22:01:29 +0200689 return parse_construct(self._construct, raw_bin_data)
Harald Weltefb506212021-05-29 21:28:24 +0200690 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +0100691 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +0100692 t.from_tlv(raw_bin_data)
693 return t.to_dict()
Harald Welteb2edd142021-01-08 23:29:35 +0100694 return {'raw': raw_bin_data.hex()}
695
Harald Weltec91085e2022-02-10 18:05:45 +0100696 def decode_hex(self, raw_hex_data: str) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +0200697 """Decode raw (hex string) data into abstract representation.
698
699 A derived class would typically provide a _decode_bin() or _decode_hex() method
700 for implementing this specifically for the given file. This function checks which
701 of the method exists, add calls them (with conversion, as needed).
702
703 Args:
704 raw_hex_data : hex-encoded data
705 Returns:
706 abstract_data; dict representing the decoded data
707 """
Harald Welteb2edd142021-01-08 23:29:35 +0100708 method = getattr(self, '_decode_hex', None)
709 if callable(method):
710 return method(raw_hex_data)
711 raw_bin_data = h2b(raw_hex_data)
712 method = getattr(self, '_decode_bin', None)
713 if callable(method):
714 return method(raw_bin_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200715 if self._construct:
Harald Welte07c7b1f2021-05-28 22:01:29 +0200716 return parse_construct(self._construct, raw_bin_data)
Harald Weltefb506212021-05-29 21:28:24 +0200717 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +0100718 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +0100719 t.from_tlv(raw_bin_data)
720 return t.to_dict()
Harald Welteb2edd142021-01-08 23:29:35 +0100721 return {'raw': raw_bin_data.hex()}
722
Harald Weltec91085e2022-02-10 18:05:45 +0100723 def encode_bin(self, abstract_data: dict) -> bytearray:
Harald Welteee3501f2021-04-02 13:00:18 +0200724 """Encode abstract representation into raw (binary) data.
725
726 A derived class would typically provide an _encode_bin() or _encode_hex() method
727 for implementing this specifically for the given file. This function checks which
728 of the method exists, add calls them (with conversion, as needed).
729
730 Args:
731 abstract_data : dict representing the decoded data
732 Returns:
733 binary encoded data
734 """
Harald Welteb2edd142021-01-08 23:29:35 +0100735 method = getattr(self, '_encode_bin', None)
736 if callable(method):
737 return method(abstract_data)
738 method = getattr(self, '_encode_hex', None)
739 if callable(method):
740 return h2b(method(abstract_data))
Harald Welte2db5cfb2021-04-10 19:05:37 +0200741 if self._construct:
742 return self._construct.build(abstract_data)
Harald Weltefb506212021-05-29 21:28:24 +0200743 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +0100744 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +0100745 t.from_dict(abstract_data)
746 return t.to_tlv()
Harald Weltec91085e2022-02-10 18:05:45 +0100747 raise NotImplementedError(
748 "%s encoder not yet implemented. Patches welcome." % self)
Harald Welteb2edd142021-01-08 23:29:35 +0100749
Harald Weltec91085e2022-02-10 18:05:45 +0100750 def encode_hex(self, abstract_data: dict) -> str:
Harald Welteee3501f2021-04-02 13:00:18 +0200751 """Encode abstract representation into raw (hex string) data.
752
753 A derived class would typically provide an _encode_bin() or _encode_hex() method
754 for implementing this specifically for the given file. This function checks which
755 of the method exists, add calls them (with conversion, as needed).
756
757 Args:
758 abstract_data : dict representing the decoded data
759 Returns:
760 hex string encoded data
761 """
Harald Welteb2edd142021-01-08 23:29:35 +0100762 method = getattr(self, '_encode_hex', None)
763 if callable(method):
764 return method(abstract_data)
765 method = getattr(self, '_encode_bin', None)
766 if callable(method):
767 raw_bin_data = method(abstract_data)
768 return b2h(raw_bin_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200769 if self._construct:
770 return b2h(self._construct.build(abstract_data))
Harald Weltefb506212021-05-29 21:28:24 +0200771 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +0100772 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +0100773 t.from_dict(abstract_data)
774 return b2h(t.to_tlv())
Harald Weltec91085e2022-02-10 18:05:45 +0100775 raise NotImplementedError(
776 "%s encoder not yet implemented. Patches welcome." % self)
Harald Welteb2edd142021-01-08 23:29:35 +0100777
778
779class LinFixedEF(CardEF):
Harald Welteee3501f2021-04-02 13:00:18 +0200780 """Linear Fixed EF (Entry File) in the smart card filesystem.
781
782 Linear Fixed EFs are record oriented files. They consist of a number of fixed-size
783 records. The records can be individually read/updated."""
Harald Welteb2edd142021-01-08 23:29:35 +0100784
785 @with_default_category('Linear Fixed EF Commands')
786 class ShellCommands(CommandSet):
Harald Welteee3501f2021-04-02 13:00:18 +0200787 """Shell commands specific for Linear Fixed EFs."""
Harald Weltec91085e2022-02-10 18:05:45 +0100788
Harald Welte9170fbf2022-02-11 21:54:37 +0100789 def __init__(self, **kwargs):
790 super().__init__(**kwargs)
Harald Welteb2edd142021-01-08 23:29:35 +0100791
Harald Welteaefd0642022-02-25 15:26:37 +0100792 dec_hex_parser = argparse.ArgumentParser()
793 dec_hex_parser.add_argument('--oneline', action='store_true',
794 help='No JSON pretty-printing, dump as a single line')
795 dec_hex_parser.add_argument('HEXSTR', help='Hex-string of encoded data to decode')
796
797 @cmd2.with_argparser(dec_hex_parser)
798 def do_decode_hex(self, opts):
799 """Decode command-line provided hex-string as if it was read from the file."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200800 data = self._cmd.lchan.selected_file.decode_record_hex(opts.HEXSTR)
Harald Welteaefd0642022-02-25 15:26:37 +0100801 self._cmd.poutput_json(data, opts.oneline)
802
Harald Welteb2edd142021-01-08 23:29:35 +0100803 read_rec_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100804 read_rec_parser.add_argument(
805 'record_nr', type=int, help='Number of record to be read')
806 read_rec_parser.add_argument(
807 '--count', type=int, default=1, help='Number of records to be read, beginning at record_nr')
808
Harald Welteb2edd142021-01-08 23:29:35 +0100809 @cmd2.with_argparser(read_rec_parser)
810 def do_read_record(self, opts):
Philipp Maier41555732021-02-25 16:52:08 +0100811 """Read one or multiple records from a record-oriented EF"""
812 for r in range(opts.count):
813 recnr = opts.record_nr + r
Harald Weltea6c0f882022-07-17 14:23:17 +0200814 (data, sw) = self._cmd.lchan.read_record(recnr)
Philipp Maier41555732021-02-25 16:52:08 +0100815 if (len(data) > 0):
Harald Weltec91085e2022-02-10 18:05:45 +0100816 recstr = str(data)
Philipp Maier41555732021-02-25 16:52:08 +0100817 else:
Harald Weltec91085e2022-02-10 18:05:45 +0100818 recstr = "(empty)"
Philipp Maier41555732021-02-25 16:52:08 +0100819 self._cmd.poutput("%03d %s" % (recnr, recstr))
Harald Welteb2edd142021-01-08 23:29:35 +0100820
821 read_rec_dec_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100822 read_rec_dec_parser.add_argument(
823 'record_nr', type=int, help='Number of record to be read')
Harald Weltebcad86c2021-04-06 20:08:39 +0200824 read_rec_dec_parser.add_argument('--oneline', action='store_true',
825 help='No JSON pretty-printing, dump as a single line')
Harald Weltec91085e2022-02-10 18:05:45 +0100826
Harald Welteb2edd142021-01-08 23:29:35 +0100827 @cmd2.with_argparser(read_rec_dec_parser)
828 def do_read_record_decoded(self, opts):
829 """Read + decode a record from a record-oriented EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200830 (data, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
Harald Welte1748b932021-04-06 21:12:25 +0200831 self._cmd.poutput_json(data, opts.oneline)
Harald Welteb2edd142021-01-08 23:29:35 +0100832
Harald Welte850b72a2021-04-07 09:33:03 +0200833 read_recs_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100834
Harald Welte850b72a2021-04-07 09:33:03 +0200835 @cmd2.with_argparser(read_recs_parser)
836 def do_read_records(self, opts):
837 """Read all records from a record-oriented EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200838 num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
Harald Welte850b72a2021-04-07 09:33:03 +0200839 for recnr in range(1, 1 + num_of_rec):
Harald Weltea6c0f882022-07-17 14:23:17 +0200840 (data, sw) = self._cmd.lchan.read_record(recnr)
Harald Welte850b72a2021-04-07 09:33:03 +0200841 if (len(data) > 0):
Harald Weltec91085e2022-02-10 18:05:45 +0100842 recstr = str(data)
Harald Welte850b72a2021-04-07 09:33:03 +0200843 else:
Harald Weltec91085e2022-02-10 18:05:45 +0100844 recstr = "(empty)"
Harald Welte850b72a2021-04-07 09:33:03 +0200845 self._cmd.poutput("%03d %s" % (recnr, recstr))
846
847 read_recs_dec_parser = argparse.ArgumentParser()
848 read_recs_dec_parser.add_argument('--oneline', action='store_true',
Harald Weltec91085e2022-02-10 18:05:45 +0100849 help='No JSON pretty-printing, dump as a single line')
850
Harald Welte850b72a2021-04-07 09:33:03 +0200851 @cmd2.with_argparser(read_recs_dec_parser)
852 def do_read_records_decoded(self, opts):
853 """Read + decode all records from a record-oriented EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200854 num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
Harald Welte850b72a2021-04-07 09:33:03 +0200855 # collect all results in list so they are rendered as JSON list when printing
856 data_list = []
857 for recnr in range(1, 1 + num_of_rec):
Harald Weltea6c0f882022-07-17 14:23:17 +0200858 (data, sw) = self._cmd.lchan.read_record_dec(recnr)
Harald Welte850b72a2021-04-07 09:33:03 +0200859 data_list.append(data)
860 self._cmd.poutput_json(data_list, opts.oneline)
861
Harald Welteb2edd142021-01-08 23:29:35 +0100862 upd_rec_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100863 upd_rec_parser.add_argument(
864 'record_nr', type=int, help='Number of record to be read')
865 upd_rec_parser.add_argument(
866 'data', help='Data bytes (hex format) to write')
867
Harald Welteb2edd142021-01-08 23:29:35 +0100868 @cmd2.with_argparser(upd_rec_parser)
869 def do_update_record(self, opts):
870 """Update (write) data to a record-oriented EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200871 (data, sw) = self._cmd.lchan.update_record(opts.record_nr, opts.data)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100872 if data:
873 self._cmd.poutput(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100874
875 upd_rec_dec_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100876 upd_rec_dec_parser.add_argument(
877 'record_nr', type=int, help='Number of record to be read')
878 upd_rec_dec_parser.add_argument(
879 'data', help='Abstract data (JSON format) to write')
Harald Welte0d4e98a2021-04-07 00:14:40 +0200880 upd_rec_dec_parser.add_argument('--json-path', type=str,
881 help='JSON path to modify specific element of record only')
Harald Weltec91085e2022-02-10 18:05:45 +0100882
Harald Welteb2edd142021-01-08 23:29:35 +0100883 @cmd2.with_argparser(upd_rec_dec_parser)
884 def do_update_record_decoded(self, opts):
885 """Encode + Update (write) data to a record-oriented EF"""
Harald Welte0d4e98a2021-04-07 00:14:40 +0200886 if opts.json_path:
Harald Weltea6c0f882022-07-17 14:23:17 +0200887 (data_json, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
Harald Weltec91085e2022-02-10 18:05:45 +0100888 js_path_modify(data_json, opts.json_path,
889 json.loads(opts.data))
Harald Welte0d4e98a2021-04-07 00:14:40 +0200890 else:
891 data_json = json.loads(opts.data)
Harald Weltea6c0f882022-07-17 14:23:17 +0200892 (data, sw) = self._cmd.lchan.update_record_dec(
Harald Weltec91085e2022-02-10 18:05:45 +0100893 opts.record_nr, data_json)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100894 if data:
895 self._cmd.poutput(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100896
Harald Welte4145d3c2021-04-08 20:34:13 +0200897 edit_rec_dec_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100898 edit_rec_dec_parser.add_argument(
899 'record_nr', type=int, help='Number of record to be edited')
900
Harald Welte4145d3c2021-04-08 20:34:13 +0200901 @cmd2.with_argparser(edit_rec_dec_parser)
902 def do_edit_record_decoded(self, opts):
903 """Edit the JSON representation of one record in an editor."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200904 (orig_json, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
Vadim Yanitskiy895fa6f2021-05-02 02:36:44 +0200905 with tempfile.TemporaryDirectory(prefix='pysim_') as dirname:
Harald Welte4145d3c2021-04-08 20:34:13 +0200906 filename = '%s/file' % dirname
907 # write existing data as JSON to file
908 with open(filename, 'w') as text_file:
909 json.dump(orig_json, text_file, indent=4)
910 # run a text editor
Harald Weltee1268722023-06-24 07:57:08 +0200911 self._cmd.run_editor(filename)
Harald Welte4145d3c2021-04-08 20:34:13 +0200912 with open(filename, 'r') as text_file:
913 edited_json = json.load(text_file)
914 if edited_json == orig_json:
915 self._cmd.poutput("Data not modified, skipping write")
916 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200917 (data, sw) = self._cmd.lchan.update_record_dec(
Harald Weltec91085e2022-02-10 18:05:45 +0100918 opts.record_nr, edited_json)
Harald Welte4145d3c2021-04-08 20:34:13 +0200919 if data:
920 self._cmd.poutput_json(data)
Harald Welte4145d3c2021-04-08 20:34:13 +0200921
Harald Weltec91085e2022-02-10 18:05:45 +0100922 def __init__(self, fid: str, sfid: str = None, name: str = None, desc: str = None,
Philipp Maier37e57e02023-09-07 12:43:12 +0200923 parent: Optional[CardDF] = None, rec_len: Size = (1, None), leftpad: bool = False, **kwargs):
Harald Welteee3501f2021-04-02 13:00:18 +0200924 """
925 Args:
926 fid : File Identifier (4 hex digits)
927 sfid : Short File Identifier (2 hex digits, optional)
928 name : Brief name of the file, lik EF_ICCID
Harald Weltec9cdce32021-04-11 10:28:28 +0200929 desc : Description of the file
Harald Welteee3501f2021-04-02 13:00:18 +0200930 parent : Parent CardFile object within filesystem hierarchy
Harald Welte99e4cc02022-07-21 15:25:47 +0200931 rec_len : Tuple of (minimum_length, recommended_length)
Philipp Maier37e57e02023-09-07 12:43:12 +0200932 leftpad: On write, data must be padded from the left to fit pysical record length
Harald Welteee3501f2021-04-02 13:00:18 +0200933 """
Harald Welte9170fbf2022-02-11 21:54:37 +0100934 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, **kwargs)
Harald Welteb2edd142021-01-08 23:29:35 +0100935 self.rec_len = rec_len
Philipp Maier37e57e02023-09-07 12:43:12 +0200936 self.leftpad = leftpad
Harald Welteb2edd142021-01-08 23:29:35 +0100937 self.shell_commands = [self.ShellCommands()]
Harald Welte2db5cfb2021-04-10 19:05:37 +0200938 self._construct = None
Harald Weltefb506212021-05-29 21:28:24 +0200939 self._tlv = None
Harald Welteb2edd142021-01-08 23:29:35 +0100940
Harald Weltecaa94b52023-01-31 16:43:34 +0100941 def decode_record_hex(self, raw_hex_data: str, record_nr: int = 1) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +0200942 """Decode raw (hex string) data into abstract representation.
943
944 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
945 method for implementing this specifically for the given file. This function checks which
946 of the method exists, add calls them (with conversion, as needed).
947
948 Args:
949 raw_hex_data : hex-encoded data
Harald Weltef6b37af2023-01-24 15:42:26 +0100950 record_nr : record number (1 for first record, ...)
Harald Welteee3501f2021-04-02 13:00:18 +0200951 Returns:
952 abstract_data; dict representing the decoded data
953 """
Harald Welteb2edd142021-01-08 23:29:35 +0100954 method = getattr(self, '_decode_record_hex', None)
955 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +0100956 return method(raw_hex_data, record_nr=record_nr)
Harald Welteb2edd142021-01-08 23:29:35 +0100957 raw_bin_data = h2b(raw_hex_data)
958 method = getattr(self, '_decode_record_bin', None)
959 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +0100960 return method(raw_bin_data, record_nr=record_nr)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200961 if self._construct:
Harald Welte07c7b1f2021-05-28 22:01:29 +0200962 return parse_construct(self._construct, raw_bin_data)
Harald Weltefb506212021-05-29 21:28:24 +0200963 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +0100964 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +0100965 t.from_tlv(raw_bin_data)
966 return t.to_dict()
Harald Welteb2edd142021-01-08 23:29:35 +0100967 return {'raw': raw_bin_data.hex()}
968
Harald Weltef6b37af2023-01-24 15:42:26 +0100969 def decode_record_bin(self, raw_bin_data: bytearray, record_nr: int) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +0200970 """Decode raw (binary) data into abstract representation.
971
972 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
973 method for implementing this specifically for the given file. This function checks which
974 of the method exists, add calls them (with conversion, as needed).
975
976 Args:
977 raw_bin_data : binary encoded data
Harald Weltef6b37af2023-01-24 15:42:26 +0100978 record_nr : record number (1 for first record, ...)
Harald Welteee3501f2021-04-02 13:00:18 +0200979 Returns:
980 abstract_data; dict representing the decoded data
981 """
Harald Welteb2edd142021-01-08 23:29:35 +0100982 method = getattr(self, '_decode_record_bin', None)
983 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +0100984 return method(raw_bin_data, record_nr=record_nr)
Harald Welteb2edd142021-01-08 23:29:35 +0100985 raw_hex_data = b2h(raw_bin_data)
986 method = getattr(self, '_decode_record_hex', None)
987 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +0100988 return method(raw_hex_data, record_nr=record_nr)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200989 if self._construct:
Harald Welte07c7b1f2021-05-28 22:01:29 +0200990 return parse_construct(self._construct, raw_bin_data)
Harald Weltefb506212021-05-29 21:28:24 +0200991 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +0100992 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +0100993 t.from_tlv(raw_bin_data)
994 return t.to_dict()
Harald Welteb2edd142021-01-08 23:29:35 +0100995 return {'raw': raw_hex_data}
996
Harald Weltef6b37af2023-01-24 15:42:26 +0100997 def encode_record_hex(self, abstract_data: dict, record_nr: int) -> str:
Harald Welteee3501f2021-04-02 13:00:18 +0200998 """Encode abstract representation into raw (hex string) data.
999
1000 A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
1001 method for implementing this specifically for the given file. This function checks which
1002 of the method exists, add calls them (with conversion, as needed).
1003
1004 Args:
1005 abstract_data : dict representing the decoded data
Harald Weltef6b37af2023-01-24 15:42:26 +01001006 record_nr : record number (1 for first record, ...)
Harald Welteee3501f2021-04-02 13:00:18 +02001007 Returns:
1008 hex string encoded data
1009 """
Harald Welteb2edd142021-01-08 23:29:35 +01001010 method = getattr(self, '_encode_record_hex', None)
1011 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +01001012 return method(abstract_data, record_nr=record_nr)
Harald Welteb2edd142021-01-08 23:29:35 +01001013 method = getattr(self, '_encode_record_bin', None)
1014 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +01001015 raw_bin_data = method(abstract_data, record_nr=record_nr)
Harald Welte1e456572021-04-02 17:16:30 +02001016 return b2h(raw_bin_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +02001017 if self._construct:
1018 return b2h(self._construct.build(abstract_data))
Harald Weltefb506212021-05-29 21:28:24 +02001019 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +01001020 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +01001021 t.from_dict(abstract_data)
1022 return b2h(t.to_tlv())
Harald Weltec91085e2022-02-10 18:05:45 +01001023 raise NotImplementedError(
1024 "%s encoder not yet implemented. Patches welcome." % self)
Harald Welteb2edd142021-01-08 23:29:35 +01001025
Harald Weltef6b37af2023-01-24 15:42:26 +01001026 def encode_record_bin(self, abstract_data: dict, record_nr : int) -> bytearray:
Harald Welteee3501f2021-04-02 13:00:18 +02001027 """Encode abstract representation into raw (binary) data.
1028
1029 A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
1030 method for implementing this specifically for the given file. This function checks which
1031 of the method exists, add calls them (with conversion, as needed).
1032
1033 Args:
1034 abstract_data : dict representing the decoded data
Harald Weltef6b37af2023-01-24 15:42:26 +01001035 record_nr : record number (1 for first record, ...)
Harald Welteee3501f2021-04-02 13:00:18 +02001036 Returns:
1037 binary encoded data
1038 """
Harald Welteb2edd142021-01-08 23:29:35 +01001039 method = getattr(self, '_encode_record_bin', None)
1040 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +01001041 return method(abstract_data, record_nr=record_nr)
Harald Welteb2edd142021-01-08 23:29:35 +01001042 method = getattr(self, '_encode_record_hex', None)
1043 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +01001044 return h2b(method(abstract_data, record_nr=record_nr))
Harald Welte2db5cfb2021-04-10 19:05:37 +02001045 if self._construct:
1046 return self._construct.build(abstract_data)
Harald Weltefb506212021-05-29 21:28:24 +02001047 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +01001048 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +01001049 t.from_dict(abstract_data)
1050 return t.to_tlv()
Harald Weltec91085e2022-02-10 18:05:45 +01001051 raise NotImplementedError(
1052 "%s encoder not yet implemented. Patches welcome." % self)
1053
Harald Welteb2edd142021-01-08 23:29:35 +01001054
1055class CyclicEF(LinFixedEF):
1056 """Cyclic EF (Entry File) in the smart card filesystem"""
1057 # we don't really have any special support for those; just recycling LinFixedEF here
Harald Weltec91085e2022-02-10 18:05:45 +01001058
1059 def __init__(self, fid: str, sfid: str = None, name: str = None, desc: str = None, parent: CardDF = None,
Harald Welte99e4cc02022-07-21 15:25:47 +02001060 rec_len: Size = (1, None), **kwargs):
Harald Welte9170fbf2022-02-11 21:54:37 +01001061 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, rec_len=rec_len, **kwargs)
Harald Weltec91085e2022-02-10 18:05:45 +01001062
Harald Welteb2edd142021-01-08 23:29:35 +01001063
1064class TransRecEF(TransparentEF):
1065 """Transparent EF (Entry File) containing fixed-size records.
Harald Welteee3501f2021-04-02 13:00:18 +02001066
Harald Welteb2edd142021-01-08 23:29:35 +01001067 These are the real odd-balls and mostly look like mistakes in the specification:
1068 Specified as 'transparent' EF, but actually containing several fixed-length records
1069 inside.
1070 We add a special class for those, so the user only has to provide encoder/decoder functions
1071 for a record, while this class takes care of split / merge of records.
1072 """
Harald Weltec91085e2022-02-10 18:05:45 +01001073
1074 def __init__(self, fid: str, rec_len: int, sfid: str = None, name: str = None, desc: str = None,
Harald Welte13edf302022-07-21 15:19:23 +02001075 parent: Optional[CardDF] = None, size: Size = (1, None), **kwargs):
Harald Welteee3501f2021-04-02 13:00:18 +02001076 """
1077 Args:
1078 fid : File Identifier (4 hex digits)
1079 sfid : Short File Identifier (2 hex digits, optional)
Harald Weltec9cdce32021-04-11 10:28:28 +02001080 name : Brief name of the file, like EF_ICCID
1081 desc : Description of the file
Harald Welteee3501f2021-04-02 13:00:18 +02001082 parent : Parent CardFile object within filesystem hierarchy
1083 rec_len : Length of the fixed-length records within transparent EF
1084 size : tuple of (minimum_size, recommended_size)
1085 """
Harald Welte9170fbf2022-02-11 21:54:37 +01001086 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, size=size, **kwargs)
Harald Welteb2edd142021-01-08 23:29:35 +01001087 self.rec_len = rec_len
1088
Harald Weltec91085e2022-02-10 18:05:45 +01001089 def decode_record_hex(self, raw_hex_data: str) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +02001090 """Decode raw (hex string) data into abstract representation.
1091
1092 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
1093 method for implementing this specifically for the given file. This function checks which
1094 of the method exists, add calls them (with conversion, as needed).
1095
1096 Args:
1097 raw_hex_data : hex-encoded data
1098 Returns:
1099 abstract_data; dict representing the decoded data
1100 """
Harald Welteb2edd142021-01-08 23:29:35 +01001101 method = getattr(self, '_decode_record_hex', None)
1102 if callable(method):
1103 return method(raw_hex_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +02001104 raw_bin_data = h2b(raw_hex_data)
Harald Welteb2edd142021-01-08 23:29:35 +01001105 method = getattr(self, '_decode_record_bin', None)
1106 if callable(method):
Harald Welteb2edd142021-01-08 23:29:35 +01001107 return method(raw_bin_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +02001108 if self._construct:
Harald Welte07c7b1f2021-05-28 22:01:29 +02001109 return parse_construct(self._construct, raw_bin_data)
Harald Weltefb506212021-05-29 21:28:24 +02001110 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +01001111 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +01001112 t.from_tlv(raw_bin_data)
1113 return t.to_dict()
Harald Welteb2edd142021-01-08 23:29:35 +01001114 return {'raw': raw_hex_data}
1115
Harald Weltec91085e2022-02-10 18:05:45 +01001116 def decode_record_bin(self, raw_bin_data: bytearray) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +02001117 """Decode raw (binary) data into abstract representation.
1118
1119 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
1120 method for implementing this specifically for the given file. This function checks which
1121 of the method exists, add calls them (with conversion, as needed).
1122
1123 Args:
1124 raw_bin_data : binary encoded data
1125 Returns:
1126 abstract_data; dict representing the decoded data
1127 """
Harald Welteb2edd142021-01-08 23:29:35 +01001128 method = getattr(self, '_decode_record_bin', None)
1129 if callable(method):
1130 return method(raw_bin_data)
1131 raw_hex_data = b2h(raw_bin_data)
1132 method = getattr(self, '_decode_record_hex', None)
1133 if callable(method):
1134 return method(raw_hex_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +02001135 if self._construct:
Harald Welte07c7b1f2021-05-28 22:01:29 +02001136 return parse_construct(self._construct, raw_bin_data)
Harald Weltefb506212021-05-29 21:28:24 +02001137 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +01001138 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +01001139 t.from_tlv(raw_bin_data)
1140 return t.to_dict()
Harald Welteb2edd142021-01-08 23:29:35 +01001141 return {'raw': raw_hex_data}
1142
Harald Weltec91085e2022-02-10 18:05:45 +01001143 def encode_record_hex(self, abstract_data: dict) -> str:
Harald Welteee3501f2021-04-02 13:00:18 +02001144 """Encode abstract representation into raw (hex string) data.
1145
1146 A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
1147 method for implementing this specifically for the given file. This function checks which
1148 of the method exists, add calls them (with conversion, as needed).
1149
1150 Args:
1151 abstract_data : dict representing the decoded data
1152 Returns:
1153 hex string encoded data
1154 """
Harald Welteb2edd142021-01-08 23:29:35 +01001155 method = getattr(self, '_encode_record_hex', None)
1156 if callable(method):
1157 return method(abstract_data)
1158 method = getattr(self, '_encode_record_bin', None)
1159 if callable(method):
Harald Welte1e456572021-04-02 17:16:30 +02001160 return b2h(method(abstract_data))
Harald Welte2db5cfb2021-04-10 19:05:37 +02001161 if self._construct:
1162 return b2h(filter_dict(self._construct.build(abstract_data)))
Harald Weltefb506212021-05-29 21:28:24 +02001163 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +01001164 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +01001165 t.from_dict(abstract_data)
1166 return b2h(t.to_tlv())
Harald Weltec91085e2022-02-10 18:05:45 +01001167 raise NotImplementedError(
1168 "%s encoder not yet implemented. Patches welcome." % self)
Harald Welteb2edd142021-01-08 23:29:35 +01001169
Harald Weltec91085e2022-02-10 18:05:45 +01001170 def encode_record_bin(self, abstract_data: dict) -> bytearray:
Harald Welteee3501f2021-04-02 13:00:18 +02001171 """Encode abstract representation into raw (binary) data.
1172
1173 A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
1174 method for implementing this specifically for the given file. This function checks which
1175 of the method exists, add calls them (with conversion, as needed).
1176
1177 Args:
1178 abstract_data : dict representing the decoded data
1179 Returns:
1180 binary encoded data
1181 """
Harald Welteb2edd142021-01-08 23:29:35 +01001182 method = getattr(self, '_encode_record_bin', None)
1183 if callable(method):
1184 return method(abstract_data)
1185 method = getattr(self, '_encode_record_hex', None)
1186 if callable(method):
1187 return h2b(method(abstract_data))
Harald Welte2db5cfb2021-04-10 19:05:37 +02001188 if self._construct:
1189 return filter_dict(self._construct.build(abstract_data))
Harald Weltefb506212021-05-29 21:28:24 +02001190 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +01001191 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +01001192 t.from_dict(abstract_data)
1193 return t.to_tlv()
Harald Weltec91085e2022-02-10 18:05:45 +01001194 raise NotImplementedError(
1195 "%s encoder not yet implemented. Patches welcome." % self)
Harald Welteb2edd142021-01-08 23:29:35 +01001196
Harald Weltec91085e2022-02-10 18:05:45 +01001197 def _decode_bin(self, raw_bin_data: bytearray):
1198 chunks = [raw_bin_data[i:i+self.rec_len]
1199 for i in range(0, len(raw_bin_data), self.rec_len)]
Harald Welteb2edd142021-01-08 23:29:35 +01001200 return [self.decode_record_bin(x) for x in chunks]
1201
Harald Welteee3501f2021-04-02 13:00:18 +02001202 def _encode_bin(self, abstract_data) -> bytes:
Harald Welteb2edd142021-01-08 23:29:35 +01001203 chunks = [self.encode_record_bin(x) for x in abstract_data]
1204 # FIXME: pad to file size
1205 return b''.join(chunks)
1206
1207
Harald Welte917d98c2021-04-21 11:51:25 +02001208class BerTlvEF(CardEF):
Harald Welte27881622021-04-21 11:16:31 +02001209 """BER-TLV EF (Entry File) in the smart card filesystem.
1210 A BER-TLV EF is a binary file with a BER (Basic Encoding Rules) TLV structure
Harald Welteb2edd142021-01-08 23:29:35 +01001211
Harald Welte27881622021-04-21 11:16:31 +02001212 NOTE: We currently don't really support those, this class is simply a wrapper
1213 around TransparentEF as a place-holder, so we can already define EFs of BER-TLV
1214 type without fully supporting them."""
Harald Welteb2edd142021-01-08 23:29:35 +01001215
Harald Welte917d98c2021-04-21 11:51:25 +02001216 @with_default_category('BER-TLV EF Commands')
1217 class ShellCommands(CommandSet):
1218 """Shell commands specific for BER-TLV EFs."""
Harald Weltec91085e2022-02-10 18:05:45 +01001219
Harald Welte917d98c2021-04-21 11:51:25 +02001220 def __init__(self):
1221 super().__init__()
1222
1223 retrieve_data_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +01001224 retrieve_data_parser.add_argument(
1225 'tag', type=auto_int, help='BER-TLV Tag of value to retrieve')
1226
Harald Welte917d98c2021-04-21 11:51:25 +02001227 @cmd2.with_argparser(retrieve_data_parser)
1228 def do_retrieve_data(self, opts):
1229 """Retrieve (Read) data from a BER-TLV EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +02001230 (data, sw) = self._cmd.lchan.retrieve_data(opts.tag)
Harald Welte917d98c2021-04-21 11:51:25 +02001231 self._cmd.poutput(data)
1232
1233 def do_retrieve_tags(self, opts):
1234 """List tags available in a given BER-TLV EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +02001235 tags = self._cmd.lchan.retrieve_tags()
Harald Welte917d98c2021-04-21 11:51:25 +02001236 self._cmd.poutput(tags)
1237
1238 set_data_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +01001239 set_data_parser.add_argument(
1240 'tag', type=auto_int, help='BER-TLV Tag of value to set')
1241 set_data_parser.add_argument(
1242 'data', help='Data bytes (hex format) to write')
1243
Harald Welte917d98c2021-04-21 11:51:25 +02001244 @cmd2.with_argparser(set_data_parser)
1245 def do_set_data(self, opts):
1246 """Set (Write) data for a given tag in a BER-TLV EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +02001247 (data, sw) = self._cmd.lchan.set_data(opts.tag, opts.data)
Harald Welte917d98c2021-04-21 11:51:25 +02001248 if data:
1249 self._cmd.poutput(data)
1250
1251 del_data_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +01001252 del_data_parser.add_argument(
1253 'tag', type=auto_int, help='BER-TLV Tag of value to set')
1254
Harald Welte917d98c2021-04-21 11:51:25 +02001255 @cmd2.with_argparser(del_data_parser)
1256 def do_delete_data(self, opts):
1257 """Delete data for a given tag in a BER-TLV EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +02001258 (data, sw) = self._cmd.lchan.set_data(opts.tag, None)
Harald Welte917d98c2021-04-21 11:51:25 +02001259 if data:
1260 self._cmd.poutput(data)
1261
Harald Weltec91085e2022-02-10 18:05:45 +01001262 def __init__(self, fid: str, sfid: str = None, name: str = None, desc: str = None, parent: CardDF = None,
Harald Welte13edf302022-07-21 15:19:23 +02001263 size: Size = (1, None), **kwargs):
Harald Welte917d98c2021-04-21 11:51:25 +02001264 """
1265 Args:
1266 fid : File Identifier (4 hex digits)
1267 sfid : Short File Identifier (2 hex digits, optional)
1268 name : Brief name of the file, lik EF_ICCID
1269 desc : Description of the file
1270 parent : Parent CardFile object within filesystem hierarchy
1271 size : tuple of (minimum_size, recommended_size)
1272 """
Harald Welte9170fbf2022-02-11 21:54:37 +01001273 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, **kwargs)
Harald Welte917d98c2021-04-21 11:51:25 +02001274 self._construct = None
1275 self.size = size
1276 self.shell_commands = [self.ShellCommands()]
1277
Harald Weltec91085e2022-02-10 18:05:45 +01001278def interpret_sw(sw_data: dict, sw: str):
Harald Welteee3501f2021-04-02 13:00:18 +02001279 """Interpret a given status word.
1280
1281 Args:
1282 sw_data : Hierarchical dict of status word matches
1283 sw : status word to match (string of 4 hex digits)
1284 Returns:
1285 tuple of two strings (class_string, description)
1286 """
Harald Welteb2edd142021-01-08 23:29:35 +01001287 for class_str, swdict in sw_data.items():
1288 # first try direct match
1289 if sw in swdict:
1290 return (class_str, swdict[sw])
1291 # next try wildcard matches
1292 for pattern, descr in swdict.items():
1293 if sw_match(sw, pattern):
1294 return (class_str, descr)
1295 return None
1296
Harald Weltec91085e2022-02-10 18:05:45 +01001297
Vadim Yanitskiy04b5d9d2022-07-07 03:05:30 +07001298class CardApplication:
Harald Welteb2edd142021-01-08 23:29:35 +01001299 """A card application is represented by an ADF (with contained hierarchy) and optionally
1300 some SW definitions."""
Harald Weltec91085e2022-02-10 18:05:45 +01001301
1302 def __init__(self, name, adf: Optional[CardADF] = None, aid: str = None, sw: dict = None):
Harald Welteee3501f2021-04-02 13:00:18 +02001303 """
1304 Args:
1305 adf : ADF name
1306 sw : Dict of status word conversions
1307 """
Harald Welteb2edd142021-01-08 23:29:35 +01001308 self.name = name
1309 self.adf = adf
Vadim Yanitskiy98f872b2021-03-27 01:25:46 +01001310 self.sw = sw or dict()
Harald Welte5ce35242021-04-02 20:27:05 +02001311 # back-reference from ADF to Applicaiton
1312 if self.adf:
1313 self.aid = aid or self.adf.aid
1314 self.adf.application = self
1315 else:
1316 self.aid = aid
Harald Welteb2edd142021-01-08 23:29:35 +01001317
1318 def __str__(self):
1319 return "APP(%s)" % (self.name)
1320
1321 def interpret_sw(self, sw):
Harald Welteee3501f2021-04-02 13:00:18 +02001322 """Interpret a given status word within the application.
1323
1324 Args:
Harald Weltec9cdce32021-04-11 10:28:28 +02001325 sw : Status word as string of 4 hex digits
Harald Welteee3501f2021-04-02 13:00:18 +02001326
1327 Returns:
1328 Tuple of two strings
1329 """
Harald Welteb2edd142021-01-08 23:29:35 +01001330 return interpret_sw(self.sw, sw)
1331
Harald Weltef44256c2021-10-14 15:53:39 +02001332
1333class CardModel(abc.ABC):
Harald Welte4c1dca02021-10-14 17:48:25 +02001334 """A specific card model, typically having some additional vendor-specific files. All
1335 you need to do is to define a sub-class with a list of ATRs or an overridden match
1336 method."""
Harald Weltef44256c2021-10-14 15:53:39 +02001337 _atrs = []
1338
1339 @classmethod
1340 @abc.abstractmethod
Harald Welte531894d2023-07-11 19:11:11 +02001341 def add_files(cls, rs: 'RuntimeState'):
Harald Weltef44256c2021-10-14 15:53:39 +02001342 """Add model specific files to given RuntimeState."""
1343
1344 @classmethod
Harald Weltec91085e2022-02-10 18:05:45 +01001345 def match(cls, scc: SimCardCommands) -> bool:
Harald Weltef44256c2021-10-14 15:53:39 +02001346 """Test if given card matches this model."""
1347 card_atr = scc.get_atr()
1348 for atr in cls._atrs:
1349 atr_bin = toBytes(atr)
1350 if atr_bin == card_atr:
1351 print("Detected CardModel:", cls.__name__)
1352 return True
1353 return False
1354
1355 @staticmethod
Harald Welte531894d2023-07-11 19:11:11 +02001356 def apply_matching_models(scc: SimCardCommands, rs: 'RuntimeState'):
Harald Welte4c1dca02021-10-14 17:48:25 +02001357 """Check if any of the CardModel sub-classes 'match' the currently inserted card
1358 (by ATR or overriding the 'match' method). If so, call their 'add_files'
1359 method."""
Harald Weltef44256c2021-10-14 15:53:39 +02001360 for m in CardModel.__subclasses__():
1361 if m.match(scc):
1362 m.add_files(rs)