blob: 0e72921a90f62b4ba6ee6bdcd2cc434772ec9d9f [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']]:
Philipp Maier14bf0032023-12-13 12:12:32 +0100139
140 # special-case handling for applications. Applications may be selected
141 # any time from any location. If there is an ADF somewhere in the path,
142 # we may clip everything before that ADF.
143 def clip_path(inter_path):
144 for i in reversed(range(0, len(inter_path))):
145 if isinstance(inter_path[i], CardADF):
146 return inter_path[i:]
147 return inter_path
148
Harald Welteaceb2a52022-02-12 21:41:59 +0100149 """Build the relative sequence of files we need to traverse to get from us to 'target'."""
Philipp Maiera5707c72023-12-13 12:07:24 +0100150 # special-case handling for selecting MF while the MF is selected
Harald Welte02a7f742023-07-11 09:21:52 +0200151 if target == target.get_mf():
152 return [target]
Harald Welteaceb2a52022-02-12 21:41:59 +0100153 cur_fqpath = self.fully_qualified_path_fobj()
154 target_fqpath = target.fully_qualified_path_fobj()
155 inter_path = []
Harald Welteaceb2a52022-02-12 21:41:59 +0100156 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
Philipp Maier14bf0032023-12-13 12:12:32 +0100165 return clip_path(inter_path[1:])
Harald Welteaceb2a52022-02-12 21:41:59 +0100166 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
Philipp Maiera1850ae2023-10-25 18:05:09 +0200517 def __init__(self, aid: str, has_fs: bool=False, **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
Philipp Maiera1850ae2023-10-25 18:05:09 +0200522 self.has_fs = has_fs # Flag to tell whether the ADF supports a filesystem or not
Harald Welte1e456572021-04-02 17:16:30 +0200523 mf = self.get_mf()
524 if mf:
Harald Welte5ce35242021-04-02 20:27:05 +0200525 mf.add_application_df(self)
Harald Welteb2edd142021-01-08 23:29:35 +0100526
527 def __str__(self):
Harald Welte4b003652022-07-16 11:55:07 +0200528 return "ADF(%s)" % (self.name if self.name else self.aid)
Harald Welteb2edd142021-01-08 23:29:35 +0100529
Harald Weltec91085e2022-02-10 18:05:45 +0100530 def _path_element(self, prefer_name: bool):
Harald Welteb2edd142021-01-08 23:29:35 +0100531 if self.name and prefer_name:
532 return self.name
533 else:
534 return self.aid
535
536
537class CardEF(CardFile):
538 """EF (Entry File) in the smart card filesystem"""
Harald Weltec91085e2022-02-10 18:05:45 +0100539
Harald Welteb2edd142021-01-08 23:29:35 +0100540 def __init__(self, *, fid, **kwargs):
541 kwargs['fid'] = fid
542 super().__init__(**kwargs)
543
544 def __str__(self):
545 return "EF(%s)" % (super().__str__())
546
Harald Weltec91085e2022-02-10 18:05:45 +0100547 def get_selectables(self, flags=[]) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +0200548 """Return a dict of {'identifier': File} that is selectable from the current DF.
549
550 Args:
551 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
552 If not specified, all selectables will be returned.
553 Returns:
554 dict containing all selectable items. Key is identifier (string), value
555 a reference to a CardFile (or derived class) instance.
556 """
Harald Weltec91085e2022-02-10 18:05:45 +0100557 # global selectable names + those of the parent DF
Philipp Maier786f7812021-02-25 16:48:10 +0100558 sels = super().get_selectables(flags)
Harald Welted2c177b2022-07-24 11:35:53 +0200559 if flags == [] or 'FIDS' in flags:
560 sels.update({x.fid: x for x in self.parent.children.values() if x.fid and x != self})
561 if flags == [] or 'FNAMES' in flags:
562 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 +0100563 return sels
564
565
566class TransparentEF(CardEF):
Harald Welteee3501f2021-04-02 13:00:18 +0200567 """Transparent EF (Entry File) in the smart card filesystem.
568
569 A Transparent EF is a binary file with no formal structure. This is contrary to
570 Record based EFs which have [fixed size] records that can be individually read/updated."""
Harald Welteb2edd142021-01-08 23:29:35 +0100571
572 @with_default_category('Transparent EF Commands')
573 class ShellCommands(CommandSet):
Harald Weltec9cdce32021-04-11 10:28:28 +0200574 """Shell commands specific for transparent EFs."""
Harald Weltec91085e2022-02-10 18:05:45 +0100575
Harald Welteb2edd142021-01-08 23:29:35 +0100576 def __init__(self):
577 super().__init__()
578
Harald Welteaefd0642022-02-25 15:26:37 +0100579 dec_hex_parser = argparse.ArgumentParser()
580 dec_hex_parser.add_argument('--oneline', action='store_true',
581 help='No JSON pretty-printing, dump as a single line')
582 dec_hex_parser.add_argument('HEXSTR', help='Hex-string of encoded data to decode')
583
584 @cmd2.with_argparser(dec_hex_parser)
585 def do_decode_hex(self, opts):
586 """Decode command-line provided hex-string as if it was read from the file."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200587 data = self._cmd.lchan.selected_file.decode_hex(opts.HEXSTR)
Harald Welteaefd0642022-02-25 15:26:37 +0100588 self._cmd.poutput_json(data, opts.oneline)
589
Harald Welteb2edd142021-01-08 23:29:35 +0100590 read_bin_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100591 read_bin_parser.add_argument(
592 '--offset', type=int, default=0, help='Byte offset for start of read')
593 read_bin_parser.add_argument(
594 '--length', type=int, help='Number of bytes to read')
595
Harald Welteb2edd142021-01-08 23:29:35 +0100596 @cmd2.with_argparser(read_bin_parser)
597 def do_read_binary(self, opts):
598 """Read binary data from a transparent EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200599 (data, sw) = self._cmd.lchan.read_binary(opts.length, opts.offset)
Harald Welteb2edd142021-01-08 23:29:35 +0100600 self._cmd.poutput(data)
601
Harald Weltebcad86c2021-04-06 20:08:39 +0200602 read_bin_dec_parser = argparse.ArgumentParser()
603 read_bin_dec_parser.add_argument('--oneline', action='store_true',
604 help='No JSON pretty-printing, dump as a single line')
Harald Weltec91085e2022-02-10 18:05:45 +0100605
Harald Weltebcad86c2021-04-06 20:08:39 +0200606 @cmd2.with_argparser(read_bin_dec_parser)
Harald Welteb2edd142021-01-08 23:29:35 +0100607 def do_read_binary_decoded(self, opts):
608 """Read + decode data from a transparent EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200609 (data, sw) = self._cmd.lchan.read_binary_dec()
Harald Welte1748b932021-04-06 21:12:25 +0200610 self._cmd.poutput_json(data, opts.oneline)
Harald Welteb2edd142021-01-08 23:29:35 +0100611
612 upd_bin_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100613 upd_bin_parser.add_argument(
614 '--offset', type=int, default=0, help='Byte offset for start of read')
615 upd_bin_parser.add_argument(
616 'data', help='Data bytes (hex format) to write')
617
Harald Welteb2edd142021-01-08 23:29:35 +0100618 @cmd2.with_argparser(upd_bin_parser)
619 def do_update_binary(self, opts):
620 """Update (Write) data of a transparent EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200621 (data, sw) = self._cmd.lchan.update_binary(opts.data, opts.offset)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100622 if data:
623 self._cmd.poutput(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100624
625 upd_bin_dec_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100626 upd_bin_dec_parser.add_argument(
627 'data', help='Abstract data (JSON format) to write')
Harald Welte0d4e98a2021-04-07 00:14:40 +0200628 upd_bin_dec_parser.add_argument('--json-path', type=str,
629 help='JSON path to modify specific element of file only')
Harald Weltec91085e2022-02-10 18:05:45 +0100630
Harald Welteb2edd142021-01-08 23:29:35 +0100631 @cmd2.with_argparser(upd_bin_dec_parser)
632 def do_update_binary_decoded(self, opts):
633 """Encode + Update (Write) data of a transparent EF"""
Harald Welte0d4e98a2021-04-07 00:14:40 +0200634 if opts.json_path:
Harald Weltea6c0f882022-07-17 14:23:17 +0200635 (data_json, sw) = self._cmd.lchan.read_binary_dec()
Harald Weltec91085e2022-02-10 18:05:45 +0100636 js_path_modify(data_json, opts.json_path,
637 json.loads(opts.data))
Harald Welte0d4e98a2021-04-07 00:14:40 +0200638 else:
639 data_json = json.loads(opts.data)
Harald Weltea6c0f882022-07-17 14:23:17 +0200640 (data, sw) = self._cmd.lchan.update_binary_dec(data_json)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100641 if data:
Harald Welte1748b932021-04-06 21:12:25 +0200642 self._cmd.poutput_json(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100643
Harald Welte4145d3c2021-04-08 20:34:13 +0200644 def do_edit_binary_decoded(self, opts):
645 """Edit the JSON representation of the EF contents in an editor."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200646 (orig_json, sw) = self._cmd.lchan.read_binary_dec()
Harald Welte4145d3c2021-04-08 20:34:13 +0200647 with tempfile.TemporaryDirectory(prefix='pysim_') as dirname:
648 filename = '%s/file' % dirname
649 # write existing data as JSON to file
650 with open(filename, 'w') as text_file:
651 json.dump(orig_json, text_file, indent=4)
652 # run a text editor
Harald Weltee1268722023-06-24 07:57:08 +0200653 self._cmd.run_editor(filename)
Harald Welte4145d3c2021-04-08 20:34:13 +0200654 with open(filename, 'r') as text_file:
655 edited_json = json.load(text_file)
656 if edited_json == orig_json:
657 self._cmd.poutput("Data not modified, skipping write")
658 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200659 (data, sw) = self._cmd.lchan.update_binary_dec(edited_json)
Harald Welte4145d3c2021-04-08 20:34:13 +0200660 if data:
661 self._cmd.poutput_json(data)
662
Harald Weltec91085e2022-02-10 18:05:45 +0100663 def __init__(self, fid: str, sfid: str = None, name: str = None, desc: str = None, parent: CardDF = None,
Harald Welte13edf302022-07-21 15:19:23 +0200664 size: Size = (1, None), **kwargs):
Harald Welteee3501f2021-04-02 13:00:18 +0200665 """
666 Args:
667 fid : File Identifier (4 hex digits)
668 sfid : Short File Identifier (2 hex digits, optional)
669 name : Brief name of the file, lik EF_ICCID
Harald Weltec9cdce32021-04-11 10:28:28 +0200670 desc : Description of the file
Harald Welteee3501f2021-04-02 13:00:18 +0200671 parent : Parent CardFile object within filesystem hierarchy
672 size : tuple of (minimum_size, recommended_size)
673 """
Harald Welte9170fbf2022-02-11 21:54:37 +0100674 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, **kwargs)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200675 self._construct = None
Harald Weltefb506212021-05-29 21:28:24 +0200676 self._tlv = None
Harald Welteb2edd142021-01-08 23:29:35 +0100677 self.size = size
678 self.shell_commands = [self.ShellCommands()]
679
Harald Weltec91085e2022-02-10 18:05:45 +0100680 def decode_bin(self, raw_bin_data: bytearray) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +0200681 """Decode raw (binary) data into abstract representation.
682
683 A derived class would typically provide a _decode_bin() or _decode_hex() method
684 for implementing this specifically for the given file. This function checks which
685 of the method exists, add calls them (with conversion, as needed).
686
687 Args:
688 raw_bin_data : binary encoded data
689 Returns:
690 abstract_data; dict representing the decoded data
691 """
Harald Welteb2edd142021-01-08 23:29:35 +0100692 method = getattr(self, '_decode_bin', None)
693 if callable(method):
694 return method(raw_bin_data)
695 method = getattr(self, '_decode_hex', None)
696 if callable(method):
697 return method(b2h(raw_bin_data))
Harald Welte2db5cfb2021-04-10 19:05:37 +0200698 if self._construct:
Harald Welte07c7b1f2021-05-28 22:01:29 +0200699 return parse_construct(self._construct, raw_bin_data)
Harald Weltefb506212021-05-29 21:28:24 +0200700 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +0100701 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +0100702 t.from_tlv(raw_bin_data)
703 return t.to_dict()
Harald Welteb2edd142021-01-08 23:29:35 +0100704 return {'raw': raw_bin_data.hex()}
705
Harald Weltec91085e2022-02-10 18:05:45 +0100706 def decode_hex(self, raw_hex_data: str) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +0200707 """Decode raw (hex string) data into abstract representation.
708
709 A derived class would typically provide a _decode_bin() or _decode_hex() method
710 for implementing this specifically for the given file. This function checks which
711 of the method exists, add calls them (with conversion, as needed).
712
713 Args:
714 raw_hex_data : hex-encoded data
715 Returns:
716 abstract_data; dict representing the decoded data
717 """
Harald Welteb2edd142021-01-08 23:29:35 +0100718 method = getattr(self, '_decode_hex', None)
719 if callable(method):
720 return method(raw_hex_data)
721 raw_bin_data = h2b(raw_hex_data)
722 method = getattr(self, '_decode_bin', None)
723 if callable(method):
724 return method(raw_bin_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200725 if self._construct:
Harald Welte07c7b1f2021-05-28 22:01:29 +0200726 return parse_construct(self._construct, raw_bin_data)
Harald Weltefb506212021-05-29 21:28:24 +0200727 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +0100728 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +0100729 t.from_tlv(raw_bin_data)
730 return t.to_dict()
Harald Welteb2edd142021-01-08 23:29:35 +0100731 return {'raw': raw_bin_data.hex()}
732
Harald Weltec91085e2022-02-10 18:05:45 +0100733 def encode_bin(self, abstract_data: dict) -> bytearray:
Harald Welteee3501f2021-04-02 13:00:18 +0200734 """Encode abstract representation into raw (binary) data.
735
736 A derived class would typically provide an _encode_bin() or _encode_hex() method
737 for implementing this specifically for the given file. This function checks which
738 of the method exists, add calls them (with conversion, as needed).
739
740 Args:
741 abstract_data : dict representing the decoded data
742 Returns:
743 binary encoded data
744 """
Harald Welteb2edd142021-01-08 23:29:35 +0100745 method = getattr(self, '_encode_bin', None)
746 if callable(method):
747 return method(abstract_data)
748 method = getattr(self, '_encode_hex', None)
749 if callable(method):
750 return h2b(method(abstract_data))
Harald Welte2db5cfb2021-04-10 19:05:37 +0200751 if self._construct:
752 return self._construct.build(abstract_data)
Harald Weltefb506212021-05-29 21:28:24 +0200753 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +0100754 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +0100755 t.from_dict(abstract_data)
756 return t.to_tlv()
Harald Weltec91085e2022-02-10 18:05:45 +0100757 raise NotImplementedError(
758 "%s encoder not yet implemented. Patches welcome." % self)
Harald Welteb2edd142021-01-08 23:29:35 +0100759
Harald Weltec91085e2022-02-10 18:05:45 +0100760 def encode_hex(self, abstract_data: dict) -> str:
Harald Welteee3501f2021-04-02 13:00:18 +0200761 """Encode abstract representation into raw (hex string) data.
762
763 A derived class would typically provide an _encode_bin() or _encode_hex() method
764 for implementing this specifically for the given file. This function checks which
765 of the method exists, add calls them (with conversion, as needed).
766
767 Args:
768 abstract_data : dict representing the decoded data
769 Returns:
770 hex string encoded data
771 """
Harald Welteb2edd142021-01-08 23:29:35 +0100772 method = getattr(self, '_encode_hex', None)
773 if callable(method):
774 return method(abstract_data)
775 method = getattr(self, '_encode_bin', None)
776 if callable(method):
777 raw_bin_data = method(abstract_data)
778 return b2h(raw_bin_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200779 if self._construct:
780 return b2h(self._construct.build(abstract_data))
Harald Weltefb506212021-05-29 21:28:24 +0200781 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +0100782 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +0100783 t.from_dict(abstract_data)
784 return b2h(t.to_tlv())
Harald Weltec91085e2022-02-10 18:05:45 +0100785 raise NotImplementedError(
786 "%s encoder not yet implemented. Patches welcome." % self)
Harald Welteb2edd142021-01-08 23:29:35 +0100787
788
789class LinFixedEF(CardEF):
Harald Welteee3501f2021-04-02 13:00:18 +0200790 """Linear Fixed EF (Entry File) in the smart card filesystem.
791
792 Linear Fixed EFs are record oriented files. They consist of a number of fixed-size
793 records. The records can be individually read/updated."""
Harald Welteb2edd142021-01-08 23:29:35 +0100794
795 @with_default_category('Linear Fixed EF Commands')
796 class ShellCommands(CommandSet):
Harald Welteee3501f2021-04-02 13:00:18 +0200797 """Shell commands specific for Linear Fixed EFs."""
Harald Weltec91085e2022-02-10 18:05:45 +0100798
Harald Welte9170fbf2022-02-11 21:54:37 +0100799 def __init__(self, **kwargs):
800 super().__init__(**kwargs)
Harald Welteb2edd142021-01-08 23:29:35 +0100801
Harald Welteaefd0642022-02-25 15:26:37 +0100802 dec_hex_parser = argparse.ArgumentParser()
803 dec_hex_parser.add_argument('--oneline', action='store_true',
804 help='No JSON pretty-printing, dump as a single line')
805 dec_hex_parser.add_argument('HEXSTR', help='Hex-string of encoded data to decode')
806
807 @cmd2.with_argparser(dec_hex_parser)
808 def do_decode_hex(self, opts):
809 """Decode command-line provided hex-string as if it was read from the file."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200810 data = self._cmd.lchan.selected_file.decode_record_hex(opts.HEXSTR)
Harald Welteaefd0642022-02-25 15:26:37 +0100811 self._cmd.poutput_json(data, opts.oneline)
812
Harald Welteb2edd142021-01-08 23:29:35 +0100813 read_rec_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100814 read_rec_parser.add_argument(
815 'record_nr', type=int, help='Number of record to be read')
816 read_rec_parser.add_argument(
817 '--count', type=int, default=1, help='Number of records to be read, beginning at record_nr')
818
Harald Welteb2edd142021-01-08 23:29:35 +0100819 @cmd2.with_argparser(read_rec_parser)
820 def do_read_record(self, opts):
Philipp Maier41555732021-02-25 16:52:08 +0100821 """Read one or multiple records from a record-oriented EF"""
822 for r in range(opts.count):
823 recnr = opts.record_nr + r
Harald Weltea6c0f882022-07-17 14:23:17 +0200824 (data, sw) = self._cmd.lchan.read_record(recnr)
Philipp Maier41555732021-02-25 16:52:08 +0100825 if (len(data) > 0):
Harald Weltec91085e2022-02-10 18:05:45 +0100826 recstr = str(data)
Philipp Maier41555732021-02-25 16:52:08 +0100827 else:
Harald Weltec91085e2022-02-10 18:05:45 +0100828 recstr = "(empty)"
Philipp Maier41555732021-02-25 16:52:08 +0100829 self._cmd.poutput("%03d %s" % (recnr, recstr))
Harald Welteb2edd142021-01-08 23:29:35 +0100830
831 read_rec_dec_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100832 read_rec_dec_parser.add_argument(
833 'record_nr', type=int, help='Number of record to be read')
Harald Weltebcad86c2021-04-06 20:08:39 +0200834 read_rec_dec_parser.add_argument('--oneline', action='store_true',
835 help='No JSON pretty-printing, dump as a single line')
Harald Weltec91085e2022-02-10 18:05:45 +0100836
Harald Welteb2edd142021-01-08 23:29:35 +0100837 @cmd2.with_argparser(read_rec_dec_parser)
838 def do_read_record_decoded(self, opts):
839 """Read + decode a record from a record-oriented EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200840 (data, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
Harald Welte1748b932021-04-06 21:12:25 +0200841 self._cmd.poutput_json(data, opts.oneline)
Harald Welteb2edd142021-01-08 23:29:35 +0100842
Harald Welte850b72a2021-04-07 09:33:03 +0200843 read_recs_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100844
Harald Welte850b72a2021-04-07 09:33:03 +0200845 @cmd2.with_argparser(read_recs_parser)
846 def do_read_records(self, opts):
847 """Read all records from a record-oriented EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200848 num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
Harald Welte850b72a2021-04-07 09:33:03 +0200849 for recnr in range(1, 1 + num_of_rec):
Harald Weltea6c0f882022-07-17 14:23:17 +0200850 (data, sw) = self._cmd.lchan.read_record(recnr)
Harald Welte850b72a2021-04-07 09:33:03 +0200851 if (len(data) > 0):
Harald Weltec91085e2022-02-10 18:05:45 +0100852 recstr = str(data)
Harald Welte850b72a2021-04-07 09:33:03 +0200853 else:
Harald Weltec91085e2022-02-10 18:05:45 +0100854 recstr = "(empty)"
Harald Welte850b72a2021-04-07 09:33:03 +0200855 self._cmd.poutput("%03d %s" % (recnr, recstr))
856
857 read_recs_dec_parser = argparse.ArgumentParser()
858 read_recs_dec_parser.add_argument('--oneline', action='store_true',
Harald Weltec91085e2022-02-10 18:05:45 +0100859 help='No JSON pretty-printing, dump as a single line')
860
Harald Welte850b72a2021-04-07 09:33:03 +0200861 @cmd2.with_argparser(read_recs_dec_parser)
862 def do_read_records_decoded(self, opts):
863 """Read + decode all records from a record-oriented EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200864 num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
Harald Welte850b72a2021-04-07 09:33:03 +0200865 # collect all results in list so they are rendered as JSON list when printing
866 data_list = []
867 for recnr in range(1, 1 + num_of_rec):
Harald Weltea6c0f882022-07-17 14:23:17 +0200868 (data, sw) = self._cmd.lchan.read_record_dec(recnr)
Harald Welte850b72a2021-04-07 09:33:03 +0200869 data_list.append(data)
870 self._cmd.poutput_json(data_list, opts.oneline)
871
Harald Welteb2edd142021-01-08 23:29:35 +0100872 upd_rec_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100873 upd_rec_parser.add_argument(
874 'record_nr', type=int, help='Number of record to be read')
875 upd_rec_parser.add_argument(
876 'data', help='Data bytes (hex format) to write')
877
Harald Welteb2edd142021-01-08 23:29:35 +0100878 @cmd2.with_argparser(upd_rec_parser)
879 def do_update_record(self, opts):
880 """Update (write) data to a record-oriented EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200881 (data, sw) = self._cmd.lchan.update_record(opts.record_nr, opts.data)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100882 if data:
883 self._cmd.poutput(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100884
885 upd_rec_dec_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100886 upd_rec_dec_parser.add_argument(
887 'record_nr', type=int, help='Number of record to be read')
888 upd_rec_dec_parser.add_argument(
889 'data', help='Abstract data (JSON format) to write')
Harald Welte0d4e98a2021-04-07 00:14:40 +0200890 upd_rec_dec_parser.add_argument('--json-path', type=str,
891 help='JSON path to modify specific element of record only')
Harald Weltec91085e2022-02-10 18:05:45 +0100892
Harald Welteb2edd142021-01-08 23:29:35 +0100893 @cmd2.with_argparser(upd_rec_dec_parser)
894 def do_update_record_decoded(self, opts):
895 """Encode + Update (write) data to a record-oriented EF"""
Harald Welte0d4e98a2021-04-07 00:14:40 +0200896 if opts.json_path:
Harald Weltea6c0f882022-07-17 14:23:17 +0200897 (data_json, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
Harald Weltec91085e2022-02-10 18:05:45 +0100898 js_path_modify(data_json, opts.json_path,
899 json.loads(opts.data))
Harald Welte0d4e98a2021-04-07 00:14:40 +0200900 else:
901 data_json = json.loads(opts.data)
Harald Weltea6c0f882022-07-17 14:23:17 +0200902 (data, sw) = self._cmd.lchan.update_record_dec(
Harald Weltec91085e2022-02-10 18:05:45 +0100903 opts.record_nr, data_json)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100904 if data:
905 self._cmd.poutput(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100906
Harald Welte4145d3c2021-04-08 20:34:13 +0200907 edit_rec_dec_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100908 edit_rec_dec_parser.add_argument(
909 'record_nr', type=int, help='Number of record to be edited')
910
Harald Welte4145d3c2021-04-08 20:34:13 +0200911 @cmd2.with_argparser(edit_rec_dec_parser)
912 def do_edit_record_decoded(self, opts):
913 """Edit the JSON representation of one record in an editor."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200914 (orig_json, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
Vadim Yanitskiy895fa6f2021-05-02 02:36:44 +0200915 with tempfile.TemporaryDirectory(prefix='pysim_') as dirname:
Harald Welte4145d3c2021-04-08 20:34:13 +0200916 filename = '%s/file' % dirname
917 # write existing data as JSON to file
918 with open(filename, 'w') as text_file:
919 json.dump(orig_json, text_file, indent=4)
920 # run a text editor
Harald Weltee1268722023-06-24 07:57:08 +0200921 self._cmd.run_editor(filename)
Harald Welte4145d3c2021-04-08 20:34:13 +0200922 with open(filename, 'r') as text_file:
923 edited_json = json.load(text_file)
924 if edited_json == orig_json:
925 self._cmd.poutput("Data not modified, skipping write")
926 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200927 (data, sw) = self._cmd.lchan.update_record_dec(
Harald Weltec91085e2022-02-10 18:05:45 +0100928 opts.record_nr, edited_json)
Harald Welte4145d3c2021-04-08 20:34:13 +0200929 if data:
930 self._cmd.poutput_json(data)
Harald Welte4145d3c2021-04-08 20:34:13 +0200931
Harald Weltec91085e2022-02-10 18:05:45 +0100932 def __init__(self, fid: str, sfid: str = None, name: str = None, desc: str = None,
Philipp Maier37e57e02023-09-07 12:43:12 +0200933 parent: Optional[CardDF] = None, rec_len: Size = (1, None), leftpad: bool = False, **kwargs):
Harald Welteee3501f2021-04-02 13:00:18 +0200934 """
935 Args:
936 fid : File Identifier (4 hex digits)
937 sfid : Short File Identifier (2 hex digits, optional)
938 name : Brief name of the file, lik EF_ICCID
Harald Weltec9cdce32021-04-11 10:28:28 +0200939 desc : Description of the file
Harald Welteee3501f2021-04-02 13:00:18 +0200940 parent : Parent CardFile object within filesystem hierarchy
Harald Welte99e4cc02022-07-21 15:25:47 +0200941 rec_len : Tuple of (minimum_length, recommended_length)
Philipp Maier37e57e02023-09-07 12:43:12 +0200942 leftpad: On write, data must be padded from the left to fit pysical record length
Harald Welteee3501f2021-04-02 13:00:18 +0200943 """
Harald Welte9170fbf2022-02-11 21:54:37 +0100944 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, **kwargs)
Harald Welteb2edd142021-01-08 23:29:35 +0100945 self.rec_len = rec_len
Philipp Maier37e57e02023-09-07 12:43:12 +0200946 self.leftpad = leftpad
Harald Welteb2edd142021-01-08 23:29:35 +0100947 self.shell_commands = [self.ShellCommands()]
Harald Welte2db5cfb2021-04-10 19:05:37 +0200948 self._construct = None
Harald Weltefb506212021-05-29 21:28:24 +0200949 self._tlv = None
Harald Welteb2edd142021-01-08 23:29:35 +0100950
Harald Weltecaa94b52023-01-31 16:43:34 +0100951 def decode_record_hex(self, raw_hex_data: str, record_nr: int = 1) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +0200952 """Decode raw (hex string) data into abstract representation.
953
954 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
955 method for implementing this specifically for the given file. This function checks which
956 of the method exists, add calls them (with conversion, as needed).
957
958 Args:
959 raw_hex_data : hex-encoded data
Harald Weltef6b37af2023-01-24 15:42:26 +0100960 record_nr : record number (1 for first record, ...)
Harald Welteee3501f2021-04-02 13:00:18 +0200961 Returns:
962 abstract_data; dict representing the decoded data
963 """
Harald Welteb2edd142021-01-08 23:29:35 +0100964 method = getattr(self, '_decode_record_hex', None)
965 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +0100966 return method(raw_hex_data, record_nr=record_nr)
Harald Welteb2edd142021-01-08 23:29:35 +0100967 raw_bin_data = h2b(raw_hex_data)
968 method = getattr(self, '_decode_record_bin', None)
969 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +0100970 return method(raw_bin_data, record_nr=record_nr)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200971 if self._construct:
Harald Welte07c7b1f2021-05-28 22:01:29 +0200972 return parse_construct(self._construct, raw_bin_data)
Harald Weltefb506212021-05-29 21:28:24 +0200973 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +0100974 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +0100975 t.from_tlv(raw_bin_data)
976 return t.to_dict()
Harald Welteb2edd142021-01-08 23:29:35 +0100977 return {'raw': raw_bin_data.hex()}
978
Harald Weltef6b37af2023-01-24 15:42:26 +0100979 def decode_record_bin(self, raw_bin_data: bytearray, record_nr: int) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +0200980 """Decode raw (binary) data into abstract representation.
981
982 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
983 method for implementing this specifically for the given file. This function checks which
984 of the method exists, add calls them (with conversion, as needed).
985
986 Args:
987 raw_bin_data : binary encoded data
Harald Weltef6b37af2023-01-24 15:42:26 +0100988 record_nr : record number (1 for first record, ...)
Harald Welteee3501f2021-04-02 13:00:18 +0200989 Returns:
990 abstract_data; dict representing the decoded data
991 """
Harald Welteb2edd142021-01-08 23:29:35 +0100992 method = getattr(self, '_decode_record_bin', None)
993 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +0100994 return method(raw_bin_data, record_nr=record_nr)
Harald Welteb2edd142021-01-08 23:29:35 +0100995 raw_hex_data = b2h(raw_bin_data)
996 method = getattr(self, '_decode_record_hex', None)
997 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +0100998 return method(raw_hex_data, record_nr=record_nr)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200999 if self._construct:
Harald Welte07c7b1f2021-05-28 22:01:29 +02001000 return parse_construct(self._construct, raw_bin_data)
Harald Weltefb506212021-05-29 21:28:24 +02001001 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +01001002 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +01001003 t.from_tlv(raw_bin_data)
1004 return t.to_dict()
Harald Welteb2edd142021-01-08 23:29:35 +01001005 return {'raw': raw_hex_data}
1006
Harald Weltef6b37af2023-01-24 15:42:26 +01001007 def encode_record_hex(self, abstract_data: dict, record_nr: int) -> str:
Harald Welteee3501f2021-04-02 13:00:18 +02001008 """Encode abstract representation into raw (hex string) data.
1009
1010 A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
1011 method for implementing this specifically for the given file. This function checks which
1012 of the method exists, add calls them (with conversion, as needed).
1013
1014 Args:
1015 abstract_data : dict representing the decoded data
Harald Weltef6b37af2023-01-24 15:42:26 +01001016 record_nr : record number (1 for first record, ...)
Harald Welteee3501f2021-04-02 13:00:18 +02001017 Returns:
1018 hex string encoded data
1019 """
Harald Welteb2edd142021-01-08 23:29:35 +01001020 method = getattr(self, '_encode_record_hex', None)
1021 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +01001022 return method(abstract_data, record_nr=record_nr)
Harald Welteb2edd142021-01-08 23:29:35 +01001023 method = getattr(self, '_encode_record_bin', None)
1024 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +01001025 raw_bin_data = method(abstract_data, record_nr=record_nr)
Harald Welte1e456572021-04-02 17:16:30 +02001026 return b2h(raw_bin_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +02001027 if self._construct:
1028 return b2h(self._construct.build(abstract_data))
Harald Weltefb506212021-05-29 21:28:24 +02001029 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +01001030 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +01001031 t.from_dict(abstract_data)
1032 return b2h(t.to_tlv())
Harald Weltec91085e2022-02-10 18:05:45 +01001033 raise NotImplementedError(
1034 "%s encoder not yet implemented. Patches welcome." % self)
Harald Welteb2edd142021-01-08 23:29:35 +01001035
Harald Weltef6b37af2023-01-24 15:42:26 +01001036 def encode_record_bin(self, abstract_data: dict, record_nr : int) -> bytearray:
Harald Welteee3501f2021-04-02 13:00:18 +02001037 """Encode abstract representation into raw (binary) data.
1038
1039 A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
1040 method for implementing this specifically for the given file. This function checks which
1041 of the method exists, add calls them (with conversion, as needed).
1042
1043 Args:
1044 abstract_data : dict representing the decoded data
Harald Weltef6b37af2023-01-24 15:42:26 +01001045 record_nr : record number (1 for first record, ...)
Harald Welteee3501f2021-04-02 13:00:18 +02001046 Returns:
1047 binary encoded data
1048 """
Harald Welteb2edd142021-01-08 23:29:35 +01001049 method = getattr(self, '_encode_record_bin', None)
1050 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +01001051 return method(abstract_data, record_nr=record_nr)
Harald Welteb2edd142021-01-08 23:29:35 +01001052 method = getattr(self, '_encode_record_hex', None)
1053 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +01001054 return h2b(method(abstract_data, record_nr=record_nr))
Harald Welte2db5cfb2021-04-10 19:05:37 +02001055 if self._construct:
1056 return self._construct.build(abstract_data)
Harald Weltefb506212021-05-29 21:28:24 +02001057 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +01001058 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +01001059 t.from_dict(abstract_data)
1060 return t.to_tlv()
Harald Weltec91085e2022-02-10 18:05:45 +01001061 raise NotImplementedError(
1062 "%s encoder not yet implemented. Patches welcome." % self)
1063
Harald Welteb2edd142021-01-08 23:29:35 +01001064
1065class CyclicEF(LinFixedEF):
1066 """Cyclic EF (Entry File) in the smart card filesystem"""
1067 # we don't really have any special support for those; just recycling LinFixedEF here
Harald Weltec91085e2022-02-10 18:05:45 +01001068
1069 def __init__(self, fid: str, sfid: str = None, name: str = None, desc: str = None, parent: CardDF = None,
Harald Welte99e4cc02022-07-21 15:25:47 +02001070 rec_len: Size = (1, None), **kwargs):
Harald Welte9170fbf2022-02-11 21:54:37 +01001071 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, rec_len=rec_len, **kwargs)
Harald Weltec91085e2022-02-10 18:05:45 +01001072
Harald Welteb2edd142021-01-08 23:29:35 +01001073
1074class TransRecEF(TransparentEF):
1075 """Transparent EF (Entry File) containing fixed-size records.
Harald Welteee3501f2021-04-02 13:00:18 +02001076
Harald Welteb2edd142021-01-08 23:29:35 +01001077 These are the real odd-balls and mostly look like mistakes in the specification:
1078 Specified as 'transparent' EF, but actually containing several fixed-length records
1079 inside.
1080 We add a special class for those, so the user only has to provide encoder/decoder functions
1081 for a record, while this class takes care of split / merge of records.
1082 """
Harald Weltec91085e2022-02-10 18:05:45 +01001083
1084 def __init__(self, fid: str, rec_len: int, sfid: str = None, name: str = None, desc: str = None,
Harald Welte13edf302022-07-21 15:19:23 +02001085 parent: Optional[CardDF] = None, size: Size = (1, None), **kwargs):
Harald Welteee3501f2021-04-02 13:00:18 +02001086 """
1087 Args:
1088 fid : File Identifier (4 hex digits)
1089 sfid : Short File Identifier (2 hex digits, optional)
Harald Weltec9cdce32021-04-11 10:28:28 +02001090 name : Brief name of the file, like EF_ICCID
1091 desc : Description of the file
Harald Welteee3501f2021-04-02 13:00:18 +02001092 parent : Parent CardFile object within filesystem hierarchy
1093 rec_len : Length of the fixed-length records within transparent EF
1094 size : tuple of (minimum_size, recommended_size)
1095 """
Harald Welte9170fbf2022-02-11 21:54:37 +01001096 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, size=size, **kwargs)
Harald Welteb2edd142021-01-08 23:29:35 +01001097 self.rec_len = rec_len
1098
Harald Weltec91085e2022-02-10 18:05:45 +01001099 def decode_record_hex(self, raw_hex_data: str) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +02001100 """Decode raw (hex string) data into abstract representation.
1101
1102 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
1103 method for implementing this specifically for the given file. This function checks which
1104 of the method exists, add calls them (with conversion, as needed).
1105
1106 Args:
1107 raw_hex_data : hex-encoded data
1108 Returns:
1109 abstract_data; dict representing the decoded data
1110 """
Harald Welteb2edd142021-01-08 23:29:35 +01001111 method = getattr(self, '_decode_record_hex', None)
1112 if callable(method):
1113 return method(raw_hex_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +02001114 raw_bin_data = h2b(raw_hex_data)
Harald Welteb2edd142021-01-08 23:29:35 +01001115 method = getattr(self, '_decode_record_bin', None)
1116 if callable(method):
Harald Welteb2edd142021-01-08 23:29:35 +01001117 return method(raw_bin_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +02001118 if self._construct:
Harald Welte07c7b1f2021-05-28 22:01:29 +02001119 return parse_construct(self._construct, raw_bin_data)
Harald Weltefb506212021-05-29 21:28:24 +02001120 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +01001121 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +01001122 t.from_tlv(raw_bin_data)
1123 return t.to_dict()
Harald Welteb2edd142021-01-08 23:29:35 +01001124 return {'raw': raw_hex_data}
1125
Harald Weltec91085e2022-02-10 18:05:45 +01001126 def decode_record_bin(self, raw_bin_data: bytearray) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +02001127 """Decode raw (binary) data into abstract representation.
1128
1129 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
1130 method for implementing this specifically for the given file. This function checks which
1131 of the method exists, add calls them (with conversion, as needed).
1132
1133 Args:
1134 raw_bin_data : binary encoded data
1135 Returns:
1136 abstract_data; dict representing the decoded data
1137 """
Harald Welteb2edd142021-01-08 23:29:35 +01001138 method = getattr(self, '_decode_record_bin', None)
1139 if callable(method):
1140 return method(raw_bin_data)
1141 raw_hex_data = b2h(raw_bin_data)
1142 method = getattr(self, '_decode_record_hex', None)
1143 if callable(method):
1144 return method(raw_hex_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +02001145 if self._construct:
Harald Welte07c7b1f2021-05-28 22:01:29 +02001146 return parse_construct(self._construct, raw_bin_data)
Harald Weltefb506212021-05-29 21:28:24 +02001147 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +01001148 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +01001149 t.from_tlv(raw_bin_data)
1150 return t.to_dict()
Harald Welteb2edd142021-01-08 23:29:35 +01001151 return {'raw': raw_hex_data}
1152
Harald Weltec91085e2022-02-10 18:05:45 +01001153 def encode_record_hex(self, abstract_data: dict) -> str:
Harald Welteee3501f2021-04-02 13:00:18 +02001154 """Encode abstract representation into raw (hex string) data.
1155
1156 A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
1157 method for implementing this specifically for the given file. This function checks which
1158 of the method exists, add calls them (with conversion, as needed).
1159
1160 Args:
1161 abstract_data : dict representing the decoded data
1162 Returns:
1163 hex string encoded data
1164 """
Harald Welteb2edd142021-01-08 23:29:35 +01001165 method = getattr(self, '_encode_record_hex', None)
1166 if callable(method):
1167 return method(abstract_data)
1168 method = getattr(self, '_encode_record_bin', None)
1169 if callable(method):
Harald Welte1e456572021-04-02 17:16:30 +02001170 return b2h(method(abstract_data))
Harald Welte2db5cfb2021-04-10 19:05:37 +02001171 if self._construct:
1172 return b2h(filter_dict(self._construct.build(abstract_data)))
Harald Weltefb506212021-05-29 21:28:24 +02001173 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +01001174 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +01001175 t.from_dict(abstract_data)
1176 return b2h(t.to_tlv())
Harald Weltec91085e2022-02-10 18:05:45 +01001177 raise NotImplementedError(
1178 "%s encoder not yet implemented. Patches welcome." % self)
Harald Welteb2edd142021-01-08 23:29:35 +01001179
Harald Weltec91085e2022-02-10 18:05:45 +01001180 def encode_record_bin(self, abstract_data: dict) -> bytearray:
Harald Welteee3501f2021-04-02 13:00:18 +02001181 """Encode abstract representation into raw (binary) data.
1182
1183 A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
1184 method for implementing this specifically for the given file. This function checks which
1185 of the method exists, add calls them (with conversion, as needed).
1186
1187 Args:
1188 abstract_data : dict representing the decoded data
1189 Returns:
1190 binary encoded data
1191 """
Harald Welteb2edd142021-01-08 23:29:35 +01001192 method = getattr(self, '_encode_record_bin', None)
1193 if callable(method):
1194 return method(abstract_data)
1195 method = getattr(self, '_encode_record_hex', None)
1196 if callable(method):
1197 return h2b(method(abstract_data))
Harald Welte2db5cfb2021-04-10 19:05:37 +02001198 if self._construct:
1199 return filter_dict(self._construct.build(abstract_data))
Harald Weltefb506212021-05-29 21:28:24 +02001200 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +01001201 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +01001202 t.from_dict(abstract_data)
1203 return t.to_tlv()
Harald Weltec91085e2022-02-10 18:05:45 +01001204 raise NotImplementedError(
1205 "%s encoder not yet implemented. Patches welcome." % self)
Harald Welteb2edd142021-01-08 23:29:35 +01001206
Harald Weltec91085e2022-02-10 18:05:45 +01001207 def _decode_bin(self, raw_bin_data: bytearray):
1208 chunks = [raw_bin_data[i:i+self.rec_len]
1209 for i in range(0, len(raw_bin_data), self.rec_len)]
Harald Welteb2edd142021-01-08 23:29:35 +01001210 return [self.decode_record_bin(x) for x in chunks]
1211
Harald Welteee3501f2021-04-02 13:00:18 +02001212 def _encode_bin(self, abstract_data) -> bytes:
Harald Welteb2edd142021-01-08 23:29:35 +01001213 chunks = [self.encode_record_bin(x) for x in abstract_data]
1214 # FIXME: pad to file size
1215 return b''.join(chunks)
1216
1217
Harald Welte917d98c2021-04-21 11:51:25 +02001218class BerTlvEF(CardEF):
Harald Welte27881622021-04-21 11:16:31 +02001219 """BER-TLV EF (Entry File) in the smart card filesystem.
1220 A BER-TLV EF is a binary file with a BER (Basic Encoding Rules) TLV structure
Harald Welteb2edd142021-01-08 23:29:35 +01001221
Harald Welte27881622021-04-21 11:16:31 +02001222 NOTE: We currently don't really support those, this class is simply a wrapper
1223 around TransparentEF as a place-holder, so we can already define EFs of BER-TLV
1224 type without fully supporting them."""
Harald Welteb2edd142021-01-08 23:29:35 +01001225
Harald Welte917d98c2021-04-21 11:51:25 +02001226 @with_default_category('BER-TLV EF Commands')
1227 class ShellCommands(CommandSet):
1228 """Shell commands specific for BER-TLV EFs."""
Harald Weltec91085e2022-02-10 18:05:45 +01001229
Harald Welte917d98c2021-04-21 11:51:25 +02001230 def __init__(self):
1231 super().__init__()
1232
1233 retrieve_data_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +01001234 retrieve_data_parser.add_argument(
1235 'tag', type=auto_int, help='BER-TLV Tag of value to retrieve')
1236
Harald Welte917d98c2021-04-21 11:51:25 +02001237 @cmd2.with_argparser(retrieve_data_parser)
1238 def do_retrieve_data(self, opts):
1239 """Retrieve (Read) data from a BER-TLV EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +02001240 (data, sw) = self._cmd.lchan.retrieve_data(opts.tag)
Harald Welte917d98c2021-04-21 11:51:25 +02001241 self._cmd.poutput(data)
1242
1243 def do_retrieve_tags(self, opts):
1244 """List tags available in a given BER-TLV EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +02001245 tags = self._cmd.lchan.retrieve_tags()
Harald Welte917d98c2021-04-21 11:51:25 +02001246 self._cmd.poutput(tags)
1247
1248 set_data_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +01001249 set_data_parser.add_argument(
1250 'tag', type=auto_int, help='BER-TLV Tag of value to set')
1251 set_data_parser.add_argument(
1252 'data', help='Data bytes (hex format) to write')
1253
Harald Welte917d98c2021-04-21 11:51:25 +02001254 @cmd2.with_argparser(set_data_parser)
1255 def do_set_data(self, opts):
1256 """Set (Write) data for a given tag in a BER-TLV EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +02001257 (data, sw) = self._cmd.lchan.set_data(opts.tag, opts.data)
Harald Welte917d98c2021-04-21 11:51:25 +02001258 if data:
1259 self._cmd.poutput(data)
1260
1261 del_data_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +01001262 del_data_parser.add_argument(
1263 'tag', type=auto_int, help='BER-TLV Tag of value to set')
1264
Harald Welte917d98c2021-04-21 11:51:25 +02001265 @cmd2.with_argparser(del_data_parser)
1266 def do_delete_data(self, opts):
1267 """Delete data for a given tag in a BER-TLV EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +02001268 (data, sw) = self._cmd.lchan.set_data(opts.tag, None)
Harald Welte917d98c2021-04-21 11:51:25 +02001269 if data:
1270 self._cmd.poutput(data)
1271
Harald Weltec91085e2022-02-10 18:05:45 +01001272 def __init__(self, fid: str, sfid: str = None, name: str = None, desc: str = None, parent: CardDF = None,
Harald Welte13edf302022-07-21 15:19:23 +02001273 size: Size = (1, None), **kwargs):
Harald Welte917d98c2021-04-21 11:51:25 +02001274 """
1275 Args:
1276 fid : File Identifier (4 hex digits)
1277 sfid : Short File Identifier (2 hex digits, optional)
1278 name : Brief name of the file, lik EF_ICCID
1279 desc : Description of the file
1280 parent : Parent CardFile object within filesystem hierarchy
1281 size : tuple of (minimum_size, recommended_size)
1282 """
Harald Welte9170fbf2022-02-11 21:54:37 +01001283 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, **kwargs)
Harald Welte917d98c2021-04-21 11:51:25 +02001284 self._construct = None
1285 self.size = size
1286 self.shell_commands = [self.ShellCommands()]
1287
Harald Weltec91085e2022-02-10 18:05:45 +01001288def interpret_sw(sw_data: dict, sw: str):
Harald Welteee3501f2021-04-02 13:00:18 +02001289 """Interpret a given status word.
1290
1291 Args:
1292 sw_data : Hierarchical dict of status word matches
1293 sw : status word to match (string of 4 hex digits)
1294 Returns:
1295 tuple of two strings (class_string, description)
1296 """
Harald Welteb2edd142021-01-08 23:29:35 +01001297 for class_str, swdict in sw_data.items():
1298 # first try direct match
1299 if sw in swdict:
1300 return (class_str, swdict[sw])
1301 # next try wildcard matches
1302 for pattern, descr in swdict.items():
1303 if sw_match(sw, pattern):
1304 return (class_str, descr)
1305 return None
1306
Harald Weltec91085e2022-02-10 18:05:45 +01001307
Vadim Yanitskiy04b5d9d2022-07-07 03:05:30 +07001308class CardApplication:
Harald Welteb2edd142021-01-08 23:29:35 +01001309 """A card application is represented by an ADF (with contained hierarchy) and optionally
1310 some SW definitions."""
Harald Weltec91085e2022-02-10 18:05:45 +01001311
1312 def __init__(self, name, adf: Optional[CardADF] = None, aid: str = None, sw: dict = None):
Harald Welteee3501f2021-04-02 13:00:18 +02001313 """
1314 Args:
1315 adf : ADF name
1316 sw : Dict of status word conversions
1317 """
Harald Welteb2edd142021-01-08 23:29:35 +01001318 self.name = name
1319 self.adf = adf
Vadim Yanitskiy98f872b2021-03-27 01:25:46 +01001320 self.sw = sw or dict()
Harald Welte5ce35242021-04-02 20:27:05 +02001321 # back-reference from ADF to Applicaiton
1322 if self.adf:
1323 self.aid = aid or self.adf.aid
1324 self.adf.application = self
1325 else:
1326 self.aid = aid
Harald Welteb2edd142021-01-08 23:29:35 +01001327
1328 def __str__(self):
1329 return "APP(%s)" % (self.name)
1330
1331 def interpret_sw(self, sw):
Harald Welteee3501f2021-04-02 13:00:18 +02001332 """Interpret a given status word within the application.
1333
1334 Args:
Harald Weltec9cdce32021-04-11 10:28:28 +02001335 sw : Status word as string of 4 hex digits
Harald Welteee3501f2021-04-02 13:00:18 +02001336
1337 Returns:
1338 Tuple of two strings
1339 """
Harald Welteb2edd142021-01-08 23:29:35 +01001340 return interpret_sw(self.sw, sw)
1341
Harald Weltef44256c2021-10-14 15:53:39 +02001342
1343class CardModel(abc.ABC):
Harald Welte4c1dca02021-10-14 17:48:25 +02001344 """A specific card model, typically having some additional vendor-specific files. All
1345 you need to do is to define a sub-class with a list of ATRs or an overridden match
1346 method."""
Harald Weltef44256c2021-10-14 15:53:39 +02001347 _atrs = []
1348
1349 @classmethod
1350 @abc.abstractmethod
Harald Welte531894d2023-07-11 19:11:11 +02001351 def add_files(cls, rs: 'RuntimeState'):
Harald Weltef44256c2021-10-14 15:53:39 +02001352 """Add model specific files to given RuntimeState."""
1353
1354 @classmethod
Harald Weltec91085e2022-02-10 18:05:45 +01001355 def match(cls, scc: SimCardCommands) -> bool:
Harald Weltef44256c2021-10-14 15:53:39 +02001356 """Test if given card matches this model."""
1357 card_atr = scc.get_atr()
1358 for atr in cls._atrs:
1359 atr_bin = toBytes(atr)
1360 if atr_bin == card_atr:
1361 print("Detected CardModel:", cls.__name__)
1362 return True
1363 return False
1364
1365 @staticmethod
Harald Welte531894d2023-07-11 19:11:11 +02001366 def apply_matching_models(scc: SimCardCommands, rs: 'RuntimeState'):
Harald Welte4c1dca02021-10-14 17:48:25 +02001367 """Check if any of the CardModel sub-classes 'match' the currently inserted card
1368 (by ATR or overriding the 'match' method). If so, call their 'add_files'
1369 method."""
Harald Weltef44256c2021-10-14 15:53:39 +02001370 for m in CardModel.__subclasses__():
1371 if m.match(scc):
1372 m.add_files(rs)