blob: 2bc5f0141413c2b9a2b92a1f38f52e9846a138c8 [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 Welte91842b42024-01-11 22:03:37 +010041from pySim.utils import sw_match, h2b, b2h, i2h, is_hex, auto_int, Hexstr, is_hexstr
Harald Weltea5eb9242023-12-23 10:20:20 +010042from pySim.construct import filter_dict, parse_construct, build_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')
Harald Welte91842b42024-01-11 22:03:37 +0100582 dec_hex_parser.add_argument('HEXSTR', type=is_hexstr, help='Hex-string of encoded data to decode')
Harald Welteaefd0642022-02-25 15:26:37 +0100583
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')
Harald Welte91842b42024-01-11 22:03:37 +0100615 upd_bin_parser.add_argument('data', type=is_hexstr, help='Data bytes (hex format) to write')
Harald Weltec91085e2022-02-10 18:05:45 +0100616
Harald Welteb2edd142021-01-08 23:29:35 +0100617 @cmd2.with_argparser(upd_bin_parser)
618 def do_update_binary(self, opts):
619 """Update (Write) data of a transparent EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200620 (data, sw) = self._cmd.lchan.update_binary(opts.data, opts.offset)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100621 if data:
622 self._cmd.poutput(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100623
624 upd_bin_dec_parser = argparse.ArgumentParser()
Harald Welte0311c922024-01-18 16:56:34 +0100625 upd_bin_dec_parser.add_argument('data', help='Abstract data (JSON format) to write')
Harald Welte0d4e98a2021-04-07 00:14:40 +0200626 upd_bin_dec_parser.add_argument('--json-path', type=str,
627 help='JSON path to modify specific element of file only')
Harald Weltec91085e2022-02-10 18:05:45 +0100628
Harald Welteb2edd142021-01-08 23:29:35 +0100629 @cmd2.with_argparser(upd_bin_dec_parser)
630 def do_update_binary_decoded(self, opts):
631 """Encode + Update (Write) data of a transparent EF"""
Harald Welte0d4e98a2021-04-07 00:14:40 +0200632 if opts.json_path:
Harald Weltea6c0f882022-07-17 14:23:17 +0200633 (data_json, sw) = self._cmd.lchan.read_binary_dec()
Harald Weltec91085e2022-02-10 18:05:45 +0100634 js_path_modify(data_json, opts.json_path,
635 json.loads(opts.data))
Harald Welte0d4e98a2021-04-07 00:14:40 +0200636 else:
637 data_json = json.loads(opts.data)
Harald Weltea6c0f882022-07-17 14:23:17 +0200638 (data, sw) = self._cmd.lchan.update_binary_dec(data_json)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100639 if data:
Harald Welte1748b932021-04-06 21:12:25 +0200640 self._cmd.poutput_json(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100641
Harald Welte4145d3c2021-04-08 20:34:13 +0200642 def do_edit_binary_decoded(self, opts):
643 """Edit the JSON representation of the EF contents in an editor."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200644 (orig_json, sw) = self._cmd.lchan.read_binary_dec()
Harald Welte4145d3c2021-04-08 20:34:13 +0200645 with tempfile.TemporaryDirectory(prefix='pysim_') as dirname:
646 filename = '%s/file' % dirname
647 # write existing data as JSON to file
648 with open(filename, 'w') as text_file:
649 json.dump(orig_json, text_file, indent=4)
650 # run a text editor
Harald Weltee1268722023-06-24 07:57:08 +0200651 self._cmd.run_editor(filename)
Harald Welte4145d3c2021-04-08 20:34:13 +0200652 with open(filename, 'r') as text_file:
653 edited_json = json.load(text_file)
654 if edited_json == orig_json:
655 self._cmd.poutput("Data not modified, skipping write")
656 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200657 (data, sw) = self._cmd.lchan.update_binary_dec(edited_json)
Harald Welte4145d3c2021-04-08 20:34:13 +0200658 if data:
659 self._cmd.poutput_json(data)
660
Harald Weltec91085e2022-02-10 18:05:45 +0100661 def __init__(self, fid: str, sfid: str = None, name: str = None, desc: str = None, parent: CardDF = None,
Harald Welte13edf302022-07-21 15:19:23 +0200662 size: Size = (1, None), **kwargs):
Harald Welteee3501f2021-04-02 13:00:18 +0200663 """
664 Args:
665 fid : File Identifier (4 hex digits)
666 sfid : Short File Identifier (2 hex digits, optional)
667 name : Brief name of the file, lik EF_ICCID
Harald Weltec9cdce32021-04-11 10:28:28 +0200668 desc : Description of the file
Harald Welteee3501f2021-04-02 13:00:18 +0200669 parent : Parent CardFile object within filesystem hierarchy
670 size : tuple of (minimum_size, recommended_size)
671 """
Harald Welte9170fbf2022-02-11 21:54:37 +0100672 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, **kwargs)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200673 self._construct = None
Harald Weltefb506212021-05-29 21:28:24 +0200674 self._tlv = None
Harald Welteb2edd142021-01-08 23:29:35 +0100675 self.size = size
676 self.shell_commands = [self.ShellCommands()]
677
Harald Weltec91085e2022-02-10 18:05:45 +0100678 def decode_bin(self, raw_bin_data: bytearray) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +0200679 """Decode raw (binary) data into abstract representation.
680
681 A derived class would typically provide a _decode_bin() or _decode_hex() method
682 for implementing this specifically for the given file. This function checks which
683 of the method exists, add calls them (with conversion, as needed).
684
685 Args:
686 raw_bin_data : binary encoded data
687 Returns:
688 abstract_data; dict representing the decoded data
689 """
Harald Welteb2edd142021-01-08 23:29:35 +0100690 method = getattr(self, '_decode_bin', None)
691 if callable(method):
692 return method(raw_bin_data)
693 method = getattr(self, '_decode_hex', None)
694 if callable(method):
695 return method(b2h(raw_bin_data))
Harald Welte2db5cfb2021-04-10 19:05:37 +0200696 if self._construct:
Harald Welte07c7b1f2021-05-28 22:01:29 +0200697 return parse_construct(self._construct, raw_bin_data)
Harald Weltefb506212021-05-29 21:28:24 +0200698 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +0100699 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +0100700 t.from_tlv(raw_bin_data)
701 return t.to_dict()
Harald Welteb2edd142021-01-08 23:29:35 +0100702 return {'raw': raw_bin_data.hex()}
703
Harald Weltec91085e2022-02-10 18:05:45 +0100704 def decode_hex(self, raw_hex_data: str) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +0200705 """Decode raw (hex string) data into abstract representation.
706
707 A derived class would typically provide a _decode_bin() or _decode_hex() method
708 for implementing this specifically for the given file. This function checks which
709 of the method exists, add calls them (with conversion, as needed).
710
711 Args:
712 raw_hex_data : hex-encoded data
713 Returns:
714 abstract_data; dict representing the decoded data
715 """
Harald Welteb2edd142021-01-08 23:29:35 +0100716 method = getattr(self, '_decode_hex', None)
717 if callable(method):
718 return method(raw_hex_data)
719 raw_bin_data = h2b(raw_hex_data)
720 method = getattr(self, '_decode_bin', None)
721 if callable(method):
722 return method(raw_bin_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200723 if self._construct:
Harald Welte07c7b1f2021-05-28 22:01:29 +0200724 return parse_construct(self._construct, raw_bin_data)
Harald Weltefb506212021-05-29 21:28:24 +0200725 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +0100726 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +0100727 t.from_tlv(raw_bin_data)
728 return t.to_dict()
Harald Welteb2edd142021-01-08 23:29:35 +0100729 return {'raw': raw_bin_data.hex()}
730
Harald Weltec91085e2022-02-10 18:05:45 +0100731 def encode_bin(self, abstract_data: dict) -> bytearray:
Harald Welteee3501f2021-04-02 13:00:18 +0200732 """Encode abstract representation into raw (binary) data.
733
734 A derived class would typically provide an _encode_bin() or _encode_hex() method
735 for implementing this specifically for the given file. This function checks which
736 of the method exists, add calls them (with conversion, as needed).
737
738 Args:
739 abstract_data : dict representing the decoded data
740 Returns:
741 binary encoded data
742 """
Harald Welteb2edd142021-01-08 23:29:35 +0100743 method = getattr(self, '_encode_bin', None)
744 if callable(method):
745 return method(abstract_data)
746 method = getattr(self, '_encode_hex', None)
747 if callable(method):
748 return h2b(method(abstract_data))
Harald Welte2db5cfb2021-04-10 19:05:37 +0200749 if self._construct:
Harald Weltea5eb9242023-12-23 10:20:20 +0100750 return build_construct(self._construct, abstract_data)
Harald Weltefb506212021-05-29 21:28:24 +0200751 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +0100752 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +0100753 t.from_dict(abstract_data)
754 return t.to_tlv()
Harald Weltec91085e2022-02-10 18:05:45 +0100755 raise NotImplementedError(
756 "%s encoder not yet implemented. Patches welcome." % self)
Harald Welteb2edd142021-01-08 23:29:35 +0100757
Harald Weltec91085e2022-02-10 18:05:45 +0100758 def encode_hex(self, abstract_data: dict) -> str:
Harald Welteee3501f2021-04-02 13:00:18 +0200759 """Encode abstract representation into raw (hex string) data.
760
761 A derived class would typically provide an _encode_bin() or _encode_hex() method
762 for implementing this specifically for the given file. This function checks which
763 of the method exists, add calls them (with conversion, as needed).
764
765 Args:
766 abstract_data : dict representing the decoded data
767 Returns:
768 hex string encoded data
769 """
Harald Welteb2edd142021-01-08 23:29:35 +0100770 method = getattr(self, '_encode_hex', None)
771 if callable(method):
772 return method(abstract_data)
773 method = getattr(self, '_encode_bin', None)
774 if callable(method):
775 raw_bin_data = method(abstract_data)
776 return b2h(raw_bin_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200777 if self._construct:
Harald Weltea5eb9242023-12-23 10:20:20 +0100778 return b2h(build_construct(self._construct, abstract_data))
Harald Weltefb506212021-05-29 21:28:24 +0200779 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +0100780 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +0100781 t.from_dict(abstract_data)
782 return b2h(t.to_tlv())
Harald Weltec91085e2022-02-10 18:05:45 +0100783 raise NotImplementedError(
784 "%s encoder not yet implemented. Patches welcome." % self)
Harald Welteb2edd142021-01-08 23:29:35 +0100785
786
787class LinFixedEF(CardEF):
Harald Welteee3501f2021-04-02 13:00:18 +0200788 """Linear Fixed EF (Entry File) in the smart card filesystem.
789
790 Linear Fixed EFs are record oriented files. They consist of a number of fixed-size
791 records. The records can be individually read/updated."""
Harald Welteb2edd142021-01-08 23:29:35 +0100792
793 @with_default_category('Linear Fixed EF Commands')
794 class ShellCommands(CommandSet):
Harald Welteee3501f2021-04-02 13:00:18 +0200795 """Shell commands specific for Linear Fixed EFs."""
Harald Weltec91085e2022-02-10 18:05:45 +0100796
Harald Welte9170fbf2022-02-11 21:54:37 +0100797 def __init__(self, **kwargs):
798 super().__init__(**kwargs)
Harald Welteb2edd142021-01-08 23:29:35 +0100799
Harald Welteaefd0642022-02-25 15:26:37 +0100800 dec_hex_parser = argparse.ArgumentParser()
801 dec_hex_parser.add_argument('--oneline', action='store_true',
802 help='No JSON pretty-printing, dump as a single line')
Harald Welte91842b42024-01-11 22:03:37 +0100803 dec_hex_parser.add_argument('HEXSTR', type=is_hexstr, help='Hex-string of encoded data to decode')
Harald Welteaefd0642022-02-25 15:26:37 +0100804
805 @cmd2.with_argparser(dec_hex_parser)
806 def do_decode_hex(self, opts):
807 """Decode command-line provided hex-string as if it was read from the file."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200808 data = self._cmd.lchan.selected_file.decode_record_hex(opts.HEXSTR)
Harald Welteaefd0642022-02-25 15:26:37 +0100809 self._cmd.poutput_json(data, opts.oneline)
810
Harald Welteb2edd142021-01-08 23:29:35 +0100811 read_rec_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100812 read_rec_parser.add_argument(
813 'record_nr', type=int, help='Number of record to be read')
814 read_rec_parser.add_argument(
815 '--count', type=int, default=1, help='Number of records to be read, beginning at record_nr')
816
Harald Welteb2edd142021-01-08 23:29:35 +0100817 @cmd2.with_argparser(read_rec_parser)
818 def do_read_record(self, opts):
Philipp Maier41555732021-02-25 16:52:08 +0100819 """Read one or multiple records from a record-oriented EF"""
820 for r in range(opts.count):
821 recnr = opts.record_nr + r
Harald Weltea6c0f882022-07-17 14:23:17 +0200822 (data, sw) = self._cmd.lchan.read_record(recnr)
Philipp Maier41555732021-02-25 16:52:08 +0100823 if (len(data) > 0):
Harald Weltec91085e2022-02-10 18:05:45 +0100824 recstr = str(data)
Philipp Maier41555732021-02-25 16:52:08 +0100825 else:
Harald Weltec91085e2022-02-10 18:05:45 +0100826 recstr = "(empty)"
Philipp Maier41555732021-02-25 16:52:08 +0100827 self._cmd.poutput("%03d %s" % (recnr, recstr))
Harald Welteb2edd142021-01-08 23:29:35 +0100828
829 read_rec_dec_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100830 read_rec_dec_parser.add_argument(
831 'record_nr', type=int, help='Number of record to be read')
Harald Weltebcad86c2021-04-06 20:08:39 +0200832 read_rec_dec_parser.add_argument('--oneline', action='store_true',
833 help='No JSON pretty-printing, dump as a single line')
Harald Weltec91085e2022-02-10 18:05:45 +0100834
Harald Welteb2edd142021-01-08 23:29:35 +0100835 @cmd2.with_argparser(read_rec_dec_parser)
836 def do_read_record_decoded(self, opts):
837 """Read + decode a record from a record-oriented EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200838 (data, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
Harald Welte1748b932021-04-06 21:12:25 +0200839 self._cmd.poutput_json(data, opts.oneline)
Harald Welteb2edd142021-01-08 23:29:35 +0100840
Harald Welte850b72a2021-04-07 09:33:03 +0200841 read_recs_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100842
Harald Welte850b72a2021-04-07 09:33:03 +0200843 @cmd2.with_argparser(read_recs_parser)
844 def do_read_records(self, opts):
845 """Read all records from a record-oriented EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200846 num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
Harald Welte850b72a2021-04-07 09:33:03 +0200847 for recnr in range(1, 1 + num_of_rec):
Harald Weltea6c0f882022-07-17 14:23:17 +0200848 (data, sw) = self._cmd.lchan.read_record(recnr)
Harald Welte850b72a2021-04-07 09:33:03 +0200849 if (len(data) > 0):
Harald Weltec91085e2022-02-10 18:05:45 +0100850 recstr = str(data)
Harald Welte850b72a2021-04-07 09:33:03 +0200851 else:
Harald Weltec91085e2022-02-10 18:05:45 +0100852 recstr = "(empty)"
Harald Welte850b72a2021-04-07 09:33:03 +0200853 self._cmd.poutput("%03d %s" % (recnr, recstr))
854
855 read_recs_dec_parser = argparse.ArgumentParser()
856 read_recs_dec_parser.add_argument('--oneline', action='store_true',
Harald Weltec91085e2022-02-10 18:05:45 +0100857 help='No JSON pretty-printing, dump as a single line')
858
Harald Welte850b72a2021-04-07 09:33:03 +0200859 @cmd2.with_argparser(read_recs_dec_parser)
860 def do_read_records_decoded(self, opts):
861 """Read + decode all records from a record-oriented EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200862 num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
Harald Welte850b72a2021-04-07 09:33:03 +0200863 # collect all results in list so they are rendered as JSON list when printing
864 data_list = []
865 for recnr in range(1, 1 + num_of_rec):
Harald Weltea6c0f882022-07-17 14:23:17 +0200866 (data, sw) = self._cmd.lchan.read_record_dec(recnr)
Harald Welte850b72a2021-04-07 09:33:03 +0200867 data_list.append(data)
868 self._cmd.poutput_json(data_list, opts.oneline)
869
Harald Welteb2edd142021-01-08 23:29:35 +0100870 upd_rec_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100871 upd_rec_parser.add_argument(
872 'record_nr', type=int, help='Number of record to be read')
Harald Welte91842b42024-01-11 22:03:37 +0100873 upd_rec_parser.add_argument('data', type=is_hexstr, help='Data bytes (hex format) to write')
Harald Weltec91085e2022-02-10 18:05:45 +0100874
Harald Welteb2edd142021-01-08 23:29:35 +0100875 @cmd2.with_argparser(upd_rec_parser)
876 def do_update_record(self, opts):
877 """Update (write) data to a record-oriented EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200878 (data, sw) = self._cmd.lchan.update_record(opts.record_nr, opts.data)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100879 if data:
880 self._cmd.poutput(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100881
882 upd_rec_dec_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100883 upd_rec_dec_parser.add_argument(
884 'record_nr', type=int, help='Number of record to be read')
Harald Welte0311c922024-01-18 16:56:34 +0100885 upd_rec_dec_parser.add_argument('data', help='Abstract data (JSON format) to write')
Harald Welte0d4e98a2021-04-07 00:14:40 +0200886 upd_rec_dec_parser.add_argument('--json-path', type=str,
887 help='JSON path to modify specific element of record only')
Harald Weltec91085e2022-02-10 18:05:45 +0100888
Harald Welteb2edd142021-01-08 23:29:35 +0100889 @cmd2.with_argparser(upd_rec_dec_parser)
890 def do_update_record_decoded(self, opts):
891 """Encode + Update (write) data to a record-oriented EF"""
Harald Welte0d4e98a2021-04-07 00:14:40 +0200892 if opts.json_path:
Harald Weltea6c0f882022-07-17 14:23:17 +0200893 (data_json, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
Harald Weltec91085e2022-02-10 18:05:45 +0100894 js_path_modify(data_json, opts.json_path,
895 json.loads(opts.data))
Harald Welte0d4e98a2021-04-07 00:14:40 +0200896 else:
897 data_json = json.loads(opts.data)
Harald Weltea6c0f882022-07-17 14:23:17 +0200898 (data, sw) = self._cmd.lchan.update_record_dec(
Harald Weltec91085e2022-02-10 18:05:45 +0100899 opts.record_nr, data_json)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100900 if data:
901 self._cmd.poutput(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100902
Harald Welte4145d3c2021-04-08 20:34:13 +0200903 edit_rec_dec_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +0100904 edit_rec_dec_parser.add_argument(
905 'record_nr', type=int, help='Number of record to be edited')
906
Harald Welte4145d3c2021-04-08 20:34:13 +0200907 @cmd2.with_argparser(edit_rec_dec_parser)
908 def do_edit_record_decoded(self, opts):
909 """Edit the JSON representation of one record in an editor."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200910 (orig_json, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
Vadim Yanitskiy895fa6f2021-05-02 02:36:44 +0200911 with tempfile.TemporaryDirectory(prefix='pysim_') as dirname:
Harald Welte4145d3c2021-04-08 20:34:13 +0200912 filename = '%s/file' % dirname
913 # write existing data as JSON to file
914 with open(filename, 'w') as text_file:
915 json.dump(orig_json, text_file, indent=4)
916 # run a text editor
Harald Weltee1268722023-06-24 07:57:08 +0200917 self._cmd.run_editor(filename)
Harald Welte4145d3c2021-04-08 20:34:13 +0200918 with open(filename, 'r') as text_file:
919 edited_json = json.load(text_file)
920 if edited_json == orig_json:
921 self._cmd.poutput("Data not modified, skipping write")
922 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200923 (data, sw) = self._cmd.lchan.update_record_dec(
Harald Weltec91085e2022-02-10 18:05:45 +0100924 opts.record_nr, edited_json)
Harald Welte4145d3c2021-04-08 20:34:13 +0200925 if data:
926 self._cmd.poutput_json(data)
Harald Welte4145d3c2021-04-08 20:34:13 +0200927
Harald Weltec91085e2022-02-10 18:05:45 +0100928 def __init__(self, fid: str, sfid: str = None, name: str = None, desc: str = None,
Philipp Maier37e57e02023-09-07 12:43:12 +0200929 parent: Optional[CardDF] = None, rec_len: Size = (1, None), leftpad: bool = False, **kwargs):
Harald Welteee3501f2021-04-02 13:00:18 +0200930 """
931 Args:
932 fid : File Identifier (4 hex digits)
933 sfid : Short File Identifier (2 hex digits, optional)
934 name : Brief name of the file, lik EF_ICCID
Harald Weltec9cdce32021-04-11 10:28:28 +0200935 desc : Description of the file
Harald Welteee3501f2021-04-02 13:00:18 +0200936 parent : Parent CardFile object within filesystem hierarchy
Harald Welte99e4cc02022-07-21 15:25:47 +0200937 rec_len : Tuple of (minimum_length, recommended_length)
Philipp Maier37e57e02023-09-07 12:43:12 +0200938 leftpad: On write, data must be padded from the left to fit pysical record length
Harald Welteee3501f2021-04-02 13:00:18 +0200939 """
Harald Welte9170fbf2022-02-11 21:54:37 +0100940 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, **kwargs)
Harald Welteb2edd142021-01-08 23:29:35 +0100941 self.rec_len = rec_len
Philipp Maier37e57e02023-09-07 12:43:12 +0200942 self.leftpad = leftpad
Harald Welteb2edd142021-01-08 23:29:35 +0100943 self.shell_commands = [self.ShellCommands()]
Harald Welte2db5cfb2021-04-10 19:05:37 +0200944 self._construct = None
Harald Weltefb506212021-05-29 21:28:24 +0200945 self._tlv = None
Harald Welteb2edd142021-01-08 23:29:35 +0100946
Harald Weltecaa94b52023-01-31 16:43:34 +0100947 def decode_record_hex(self, raw_hex_data: str, record_nr: int = 1) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +0200948 """Decode raw (hex string) data into abstract representation.
949
950 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
951 method for implementing this specifically for the given file. This function checks which
952 of the method exists, add calls them (with conversion, as needed).
953
954 Args:
955 raw_hex_data : hex-encoded data
Harald Weltef6b37af2023-01-24 15:42:26 +0100956 record_nr : record number (1 for first record, ...)
Harald Welteee3501f2021-04-02 13:00:18 +0200957 Returns:
958 abstract_data; dict representing the decoded data
959 """
Harald Welteb2edd142021-01-08 23:29:35 +0100960 method = getattr(self, '_decode_record_hex', None)
961 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +0100962 return method(raw_hex_data, record_nr=record_nr)
Harald Welteb2edd142021-01-08 23:29:35 +0100963 raw_bin_data = h2b(raw_hex_data)
964 method = getattr(self, '_decode_record_bin', None)
965 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +0100966 return method(raw_bin_data, record_nr=record_nr)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200967 if self._construct:
Harald Welte07c7b1f2021-05-28 22:01:29 +0200968 return parse_construct(self._construct, raw_bin_data)
Harald Weltefb506212021-05-29 21:28:24 +0200969 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +0100970 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +0100971 t.from_tlv(raw_bin_data)
972 return t.to_dict()
Harald Welteb2edd142021-01-08 23:29:35 +0100973 return {'raw': raw_bin_data.hex()}
974
Harald Weltef6b37af2023-01-24 15:42:26 +0100975 def decode_record_bin(self, raw_bin_data: bytearray, record_nr: int) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +0200976 """Decode raw (binary) data into abstract representation.
977
978 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
979 method for implementing this specifically for the given file. This function checks which
980 of the method exists, add calls them (with conversion, as needed).
981
982 Args:
983 raw_bin_data : binary encoded data
Harald Weltef6b37af2023-01-24 15:42:26 +0100984 record_nr : record number (1 for first record, ...)
Harald Welteee3501f2021-04-02 13:00:18 +0200985 Returns:
986 abstract_data; dict representing the decoded data
987 """
Harald Welteb2edd142021-01-08 23:29:35 +0100988 method = getattr(self, '_decode_record_bin', None)
989 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +0100990 return method(raw_bin_data, record_nr=record_nr)
Harald Welteb2edd142021-01-08 23:29:35 +0100991 raw_hex_data = b2h(raw_bin_data)
992 method = getattr(self, '_decode_record_hex', None)
993 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +0100994 return method(raw_hex_data, record_nr=record_nr)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200995 if self._construct:
Harald Welte07c7b1f2021-05-28 22:01:29 +0200996 return parse_construct(self._construct, raw_bin_data)
Harald Weltefb506212021-05-29 21:28:24 +0200997 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +0100998 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +0100999 t.from_tlv(raw_bin_data)
1000 return t.to_dict()
Harald Welteb2edd142021-01-08 23:29:35 +01001001 return {'raw': raw_hex_data}
1002
Harald Weltef6b37af2023-01-24 15:42:26 +01001003 def encode_record_hex(self, abstract_data: dict, record_nr: int) -> str:
Harald Welteee3501f2021-04-02 13:00:18 +02001004 """Encode abstract representation into raw (hex string) data.
1005
1006 A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
1007 method for implementing this specifically for the given file. This function checks which
1008 of the method exists, add calls them (with conversion, as needed).
1009
1010 Args:
1011 abstract_data : dict representing the decoded data
Harald Weltef6b37af2023-01-24 15:42:26 +01001012 record_nr : record number (1 for first record, ...)
Harald Welteee3501f2021-04-02 13:00:18 +02001013 Returns:
1014 hex string encoded data
1015 """
Harald Welteb2edd142021-01-08 23:29:35 +01001016 method = getattr(self, '_encode_record_hex', None)
1017 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +01001018 return method(abstract_data, record_nr=record_nr)
Harald Welteb2edd142021-01-08 23:29:35 +01001019 method = getattr(self, '_encode_record_bin', None)
1020 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +01001021 raw_bin_data = method(abstract_data, record_nr=record_nr)
Harald Welte1e456572021-04-02 17:16:30 +02001022 return b2h(raw_bin_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +02001023 if self._construct:
Harald Weltea5eb9242023-12-23 10:20:20 +01001024 return b2h(build_construct(self._construct, abstract_data))
Harald Weltefb506212021-05-29 21:28:24 +02001025 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +01001026 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +01001027 t.from_dict(abstract_data)
1028 return b2h(t.to_tlv())
Harald Weltec91085e2022-02-10 18:05:45 +01001029 raise NotImplementedError(
1030 "%s encoder not yet implemented. Patches welcome." % self)
Harald Welteb2edd142021-01-08 23:29:35 +01001031
Harald Weltef6b37af2023-01-24 15:42:26 +01001032 def encode_record_bin(self, abstract_data: dict, record_nr : int) -> bytearray:
Harald Welteee3501f2021-04-02 13:00:18 +02001033 """Encode abstract representation into raw (binary) data.
1034
1035 A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
1036 method for implementing this specifically for the given file. This function checks which
1037 of the method exists, add calls them (with conversion, as needed).
1038
1039 Args:
1040 abstract_data : dict representing the decoded data
Harald Weltef6b37af2023-01-24 15:42:26 +01001041 record_nr : record number (1 for first record, ...)
Harald Welteee3501f2021-04-02 13:00:18 +02001042 Returns:
1043 binary encoded data
1044 """
Harald Welteb2edd142021-01-08 23:29:35 +01001045 method = getattr(self, '_encode_record_bin', None)
1046 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +01001047 return method(abstract_data, record_nr=record_nr)
Harald Welteb2edd142021-01-08 23:29:35 +01001048 method = getattr(self, '_encode_record_hex', None)
1049 if callable(method):
Harald Weltef6b37af2023-01-24 15:42:26 +01001050 return h2b(method(abstract_data, record_nr=record_nr))
Harald Welte2db5cfb2021-04-10 19:05:37 +02001051 if self._construct:
Harald Weltea5eb9242023-12-23 10:20:20 +01001052 return build_construct(self._construct, abstract_data)
Harald Weltefb506212021-05-29 21:28:24 +02001053 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +01001054 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +01001055 t.from_dict(abstract_data)
1056 return t.to_tlv()
Harald Weltec91085e2022-02-10 18:05:45 +01001057 raise NotImplementedError(
1058 "%s encoder not yet implemented. Patches welcome." % self)
1059
Harald Welteb2edd142021-01-08 23:29:35 +01001060
1061class CyclicEF(LinFixedEF):
1062 """Cyclic EF (Entry File) in the smart card filesystem"""
1063 # we don't really have any special support for those; just recycling LinFixedEF here
Harald Weltec91085e2022-02-10 18:05:45 +01001064
1065 def __init__(self, fid: str, sfid: str = None, name: str = None, desc: str = None, parent: CardDF = None,
Harald Welte99e4cc02022-07-21 15:25:47 +02001066 rec_len: Size = (1, None), **kwargs):
Harald Welte9170fbf2022-02-11 21:54:37 +01001067 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, rec_len=rec_len, **kwargs)
Harald Weltec91085e2022-02-10 18:05:45 +01001068
Harald Welteb2edd142021-01-08 23:29:35 +01001069
1070class TransRecEF(TransparentEF):
1071 """Transparent EF (Entry File) containing fixed-size records.
Harald Welteee3501f2021-04-02 13:00:18 +02001072
Harald Welteb2edd142021-01-08 23:29:35 +01001073 These are the real odd-balls and mostly look like mistakes in the specification:
1074 Specified as 'transparent' EF, but actually containing several fixed-length records
1075 inside.
1076 We add a special class for those, so the user only has to provide encoder/decoder functions
1077 for a record, while this class takes care of split / merge of records.
1078 """
Harald Weltec91085e2022-02-10 18:05:45 +01001079
1080 def __init__(self, fid: str, rec_len: int, sfid: str = None, name: str = None, desc: str = None,
Harald Welte13edf302022-07-21 15:19:23 +02001081 parent: Optional[CardDF] = None, size: Size = (1, None), **kwargs):
Harald Welteee3501f2021-04-02 13:00:18 +02001082 """
1083 Args:
1084 fid : File Identifier (4 hex digits)
1085 sfid : Short File Identifier (2 hex digits, optional)
Harald Weltec9cdce32021-04-11 10:28:28 +02001086 name : Brief name of the file, like EF_ICCID
1087 desc : Description of the file
Harald Welteee3501f2021-04-02 13:00:18 +02001088 parent : Parent CardFile object within filesystem hierarchy
1089 rec_len : Length of the fixed-length records within transparent EF
1090 size : tuple of (minimum_size, recommended_size)
1091 """
Harald Welte9170fbf2022-02-11 21:54:37 +01001092 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, size=size, **kwargs)
Harald Welteb2edd142021-01-08 23:29:35 +01001093 self.rec_len = rec_len
1094
Harald Weltec91085e2022-02-10 18:05:45 +01001095 def decode_record_hex(self, raw_hex_data: str) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +02001096 """Decode raw (hex string) data into abstract representation.
1097
1098 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
1099 method for implementing this specifically for the given file. This function checks which
1100 of the method exists, add calls them (with conversion, as needed).
1101
1102 Args:
1103 raw_hex_data : hex-encoded data
1104 Returns:
1105 abstract_data; dict representing the decoded data
1106 """
Harald Welteb2edd142021-01-08 23:29:35 +01001107 method = getattr(self, '_decode_record_hex', None)
1108 if callable(method):
1109 return method(raw_hex_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +02001110 raw_bin_data = h2b(raw_hex_data)
Harald Welteb2edd142021-01-08 23:29:35 +01001111 method = getattr(self, '_decode_record_bin', None)
1112 if callable(method):
Harald Welteb2edd142021-01-08 23:29:35 +01001113 return method(raw_bin_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +02001114 if self._construct:
Harald Welte07c7b1f2021-05-28 22:01:29 +02001115 return parse_construct(self._construct, raw_bin_data)
Harald Weltefb506212021-05-29 21:28:24 +02001116 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +01001117 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +01001118 t.from_tlv(raw_bin_data)
1119 return t.to_dict()
Harald Welteb2edd142021-01-08 23:29:35 +01001120 return {'raw': raw_hex_data}
1121
Harald Weltec91085e2022-02-10 18:05:45 +01001122 def decode_record_bin(self, raw_bin_data: bytearray) -> dict:
Harald Welteee3501f2021-04-02 13:00:18 +02001123 """Decode raw (binary) data into abstract representation.
1124
1125 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
1126 method for implementing this specifically for the given file. This function checks which
1127 of the method exists, add calls them (with conversion, as needed).
1128
1129 Args:
1130 raw_bin_data : binary encoded data
1131 Returns:
1132 abstract_data; dict representing the decoded data
1133 """
Harald Welteb2edd142021-01-08 23:29:35 +01001134 method = getattr(self, '_decode_record_bin', None)
1135 if callable(method):
1136 return method(raw_bin_data)
1137 raw_hex_data = b2h(raw_bin_data)
1138 method = getattr(self, '_decode_record_hex', None)
1139 if callable(method):
1140 return method(raw_hex_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +02001141 if self._construct:
Harald Welte07c7b1f2021-05-28 22:01:29 +02001142 return parse_construct(self._construct, raw_bin_data)
Harald Weltefb506212021-05-29 21:28:24 +02001143 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +01001144 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +01001145 t.from_tlv(raw_bin_data)
1146 return t.to_dict()
Harald Welteb2edd142021-01-08 23:29:35 +01001147 return {'raw': raw_hex_data}
1148
Harald Weltec91085e2022-02-10 18:05:45 +01001149 def encode_record_hex(self, abstract_data: dict) -> str:
Harald Welteee3501f2021-04-02 13:00:18 +02001150 """Encode abstract representation into raw (hex string) data.
1151
1152 A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
1153 method for implementing this specifically for the given file. This function checks which
1154 of the method exists, add calls them (with conversion, as needed).
1155
1156 Args:
1157 abstract_data : dict representing the decoded data
1158 Returns:
1159 hex string encoded data
1160 """
Harald Welteb2edd142021-01-08 23:29:35 +01001161 method = getattr(self, '_encode_record_hex', None)
1162 if callable(method):
1163 return method(abstract_data)
1164 method = getattr(self, '_encode_record_bin', None)
1165 if callable(method):
Harald Welte1e456572021-04-02 17:16:30 +02001166 return b2h(method(abstract_data))
Harald Welte2db5cfb2021-04-10 19:05:37 +02001167 if self._construct:
Harald Weltea5eb9242023-12-23 10:20:20 +01001168 return b2h(filter_dict(build_construct(self._construct, abstract_data)))
Harald Weltefb506212021-05-29 21:28:24 +02001169 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +01001170 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +01001171 t.from_dict(abstract_data)
1172 return b2h(t.to_tlv())
Harald Weltec91085e2022-02-10 18:05:45 +01001173 raise NotImplementedError(
1174 "%s encoder not yet implemented. Patches welcome." % self)
Harald Welteb2edd142021-01-08 23:29:35 +01001175
Harald Weltec91085e2022-02-10 18:05:45 +01001176 def encode_record_bin(self, abstract_data: dict) -> bytearray:
Harald Welteee3501f2021-04-02 13:00:18 +02001177 """Encode abstract representation into raw (binary) data.
1178
1179 A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
1180 method for implementing this specifically for the given file. This function checks which
1181 of the method exists, add calls them (with conversion, as needed).
1182
1183 Args:
1184 abstract_data : dict representing the decoded data
1185 Returns:
1186 binary encoded data
1187 """
Harald Welteb2edd142021-01-08 23:29:35 +01001188 method = getattr(self, '_encode_record_bin', None)
1189 if callable(method):
1190 return method(abstract_data)
1191 method = getattr(self, '_encode_record_hex', None)
1192 if callable(method):
1193 return h2b(method(abstract_data))
Harald Welte2db5cfb2021-04-10 19:05:37 +02001194 if self._construct:
Harald Weltea5eb9242023-12-23 10:20:20 +01001195 return filter_dict(build_construct(self._construct, abstract_data))
Harald Weltefb506212021-05-29 21:28:24 +02001196 elif self._tlv:
Harald Welteca60ac22022-02-10 18:01:02 +01001197 t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
Harald Welte944cd2f2022-01-21 16:01:29 +01001198 t.from_dict(abstract_data)
1199 return t.to_tlv()
Harald Weltec91085e2022-02-10 18:05:45 +01001200 raise NotImplementedError(
1201 "%s encoder not yet implemented. Patches welcome." % self)
Harald Welteb2edd142021-01-08 23:29:35 +01001202
Harald Weltec91085e2022-02-10 18:05:45 +01001203 def _decode_bin(self, raw_bin_data: bytearray):
1204 chunks = [raw_bin_data[i:i+self.rec_len]
1205 for i in range(0, len(raw_bin_data), self.rec_len)]
Harald Welteb2edd142021-01-08 23:29:35 +01001206 return [self.decode_record_bin(x) for x in chunks]
1207
Harald Welteee3501f2021-04-02 13:00:18 +02001208 def _encode_bin(self, abstract_data) -> bytes:
Harald Welteb2edd142021-01-08 23:29:35 +01001209 chunks = [self.encode_record_bin(x) for x in abstract_data]
1210 # FIXME: pad to file size
1211 return b''.join(chunks)
1212
1213
Harald Welte917d98c2021-04-21 11:51:25 +02001214class BerTlvEF(CardEF):
Harald Welte27881622021-04-21 11:16:31 +02001215 """BER-TLV EF (Entry File) in the smart card filesystem.
1216 A BER-TLV EF is a binary file with a BER (Basic Encoding Rules) TLV structure
Harald Welteb2edd142021-01-08 23:29:35 +01001217
Harald Welte27881622021-04-21 11:16:31 +02001218 NOTE: We currently don't really support those, this class is simply a wrapper
1219 around TransparentEF as a place-holder, so we can already define EFs of BER-TLV
1220 type without fully supporting them."""
Harald Welteb2edd142021-01-08 23:29:35 +01001221
Harald Welte917d98c2021-04-21 11:51:25 +02001222 @with_default_category('BER-TLV EF Commands')
1223 class ShellCommands(CommandSet):
1224 """Shell commands specific for BER-TLV EFs."""
Harald Weltec91085e2022-02-10 18:05:45 +01001225
Harald Welte917d98c2021-04-21 11:51:25 +02001226 def __init__(self):
1227 super().__init__()
1228
1229 retrieve_data_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +01001230 retrieve_data_parser.add_argument(
1231 'tag', type=auto_int, help='BER-TLV Tag of value to retrieve')
1232
Harald Welte917d98c2021-04-21 11:51:25 +02001233 @cmd2.with_argparser(retrieve_data_parser)
1234 def do_retrieve_data(self, opts):
1235 """Retrieve (Read) data from a BER-TLV EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +02001236 (data, sw) = self._cmd.lchan.retrieve_data(opts.tag)
Harald Welte917d98c2021-04-21 11:51:25 +02001237 self._cmd.poutput(data)
1238
1239 def do_retrieve_tags(self, opts):
1240 """List tags available in a given BER-TLV EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +02001241 tags = self._cmd.lchan.retrieve_tags()
Harald Welte917d98c2021-04-21 11:51:25 +02001242 self._cmd.poutput(tags)
1243
1244 set_data_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +01001245 set_data_parser.add_argument(
1246 'tag', type=auto_int, help='BER-TLV Tag of value to set')
Harald Welte91842b42024-01-11 22:03:37 +01001247 set_data_parser.add_argument('data', type=is_hexstr, help='Data bytes (hex format) to write')
Harald Weltec91085e2022-02-10 18:05:45 +01001248
Harald Welte917d98c2021-04-21 11:51:25 +02001249 @cmd2.with_argparser(set_data_parser)
1250 def do_set_data(self, opts):
1251 """Set (Write) data for a given tag in a BER-TLV EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +02001252 (data, sw) = self._cmd.lchan.set_data(opts.tag, opts.data)
Harald Welte917d98c2021-04-21 11:51:25 +02001253 if data:
1254 self._cmd.poutput(data)
1255
1256 del_data_parser = argparse.ArgumentParser()
Harald Weltec91085e2022-02-10 18:05:45 +01001257 del_data_parser.add_argument(
1258 'tag', type=auto_int, help='BER-TLV Tag of value to set')
1259
Harald Welte917d98c2021-04-21 11:51:25 +02001260 @cmd2.with_argparser(del_data_parser)
1261 def do_delete_data(self, opts):
1262 """Delete data for a given tag in a BER-TLV EF"""
Harald Weltea6c0f882022-07-17 14:23:17 +02001263 (data, sw) = self._cmd.lchan.set_data(opts.tag, None)
Harald Welte917d98c2021-04-21 11:51:25 +02001264 if data:
1265 self._cmd.poutput(data)
1266
Harald Weltec91085e2022-02-10 18:05:45 +01001267 def __init__(self, fid: str, sfid: str = None, name: str = None, desc: str = None, parent: CardDF = None,
Harald Welte13edf302022-07-21 15:19:23 +02001268 size: Size = (1, None), **kwargs):
Harald Welte917d98c2021-04-21 11:51:25 +02001269 """
1270 Args:
1271 fid : File Identifier (4 hex digits)
1272 sfid : Short File Identifier (2 hex digits, optional)
1273 name : Brief name of the file, lik EF_ICCID
1274 desc : Description of the file
1275 parent : Parent CardFile object within filesystem hierarchy
1276 size : tuple of (minimum_size, recommended_size)
1277 """
Harald Welte9170fbf2022-02-11 21:54:37 +01001278 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, **kwargs)
Harald Welte917d98c2021-04-21 11:51:25 +02001279 self._construct = None
1280 self.size = size
1281 self.shell_commands = [self.ShellCommands()]
1282
Harald Weltec91085e2022-02-10 18:05:45 +01001283def interpret_sw(sw_data: dict, sw: str):
Harald Welteee3501f2021-04-02 13:00:18 +02001284 """Interpret a given status word.
1285
1286 Args:
1287 sw_data : Hierarchical dict of status word matches
1288 sw : status word to match (string of 4 hex digits)
1289 Returns:
1290 tuple of two strings (class_string, description)
1291 """
Harald Welteb2edd142021-01-08 23:29:35 +01001292 for class_str, swdict in sw_data.items():
1293 # first try direct match
1294 if sw in swdict:
1295 return (class_str, swdict[sw])
1296 # next try wildcard matches
1297 for pattern, descr in swdict.items():
1298 if sw_match(sw, pattern):
1299 return (class_str, descr)
1300 return None
1301
Harald Weltec91085e2022-02-10 18:05:45 +01001302
Vadim Yanitskiy04b5d9d2022-07-07 03:05:30 +07001303class CardApplication:
Harald Welteb2edd142021-01-08 23:29:35 +01001304 """A card application is represented by an ADF (with contained hierarchy) and optionally
1305 some SW definitions."""
Harald Weltec91085e2022-02-10 18:05:45 +01001306
1307 def __init__(self, name, adf: Optional[CardADF] = None, aid: str = None, sw: dict = None):
Harald Welteee3501f2021-04-02 13:00:18 +02001308 """
1309 Args:
1310 adf : ADF name
1311 sw : Dict of status word conversions
1312 """
Harald Welteb2edd142021-01-08 23:29:35 +01001313 self.name = name
1314 self.adf = adf
Vadim Yanitskiy98f872b2021-03-27 01:25:46 +01001315 self.sw = sw or dict()
Harald Welte5ce35242021-04-02 20:27:05 +02001316 # back-reference from ADF to Applicaiton
1317 if self.adf:
1318 self.aid = aid or self.adf.aid
1319 self.adf.application = self
1320 else:
1321 self.aid = aid
Harald Welteb2edd142021-01-08 23:29:35 +01001322
1323 def __str__(self):
1324 return "APP(%s)" % (self.name)
1325
1326 def interpret_sw(self, sw):
Harald Welteee3501f2021-04-02 13:00:18 +02001327 """Interpret a given status word within the application.
1328
1329 Args:
Harald Weltec9cdce32021-04-11 10:28:28 +02001330 sw : Status word as string of 4 hex digits
Harald Welteee3501f2021-04-02 13:00:18 +02001331
1332 Returns:
1333 Tuple of two strings
1334 """
Harald Welteb2edd142021-01-08 23:29:35 +01001335 return interpret_sw(self.sw, sw)
1336
Harald Weltef44256c2021-10-14 15:53:39 +02001337
1338class CardModel(abc.ABC):
Harald Welte4c1dca02021-10-14 17:48:25 +02001339 """A specific card model, typically having some additional vendor-specific files. All
1340 you need to do is to define a sub-class with a list of ATRs or an overridden match
1341 method."""
Harald Weltef44256c2021-10-14 15:53:39 +02001342 _atrs = []
1343
1344 @classmethod
1345 @abc.abstractmethod
Harald Welte531894d2023-07-11 19:11:11 +02001346 def add_files(cls, rs: 'RuntimeState'):
Harald Weltef44256c2021-10-14 15:53:39 +02001347 """Add model specific files to given RuntimeState."""
1348
1349 @classmethod
Harald Weltec91085e2022-02-10 18:05:45 +01001350 def match(cls, scc: SimCardCommands) -> bool:
Harald Weltef44256c2021-10-14 15:53:39 +02001351 """Test if given card matches this model."""
1352 card_atr = scc.get_atr()
1353 for atr in cls._atrs:
1354 atr_bin = toBytes(atr)
1355 if atr_bin == card_atr:
1356 print("Detected CardModel:", cls.__name__)
1357 return True
1358 return False
1359
1360 @staticmethod
Harald Welte531894d2023-07-11 19:11:11 +02001361 def apply_matching_models(scc: SimCardCommands, rs: 'RuntimeState'):
Harald Welte4c1dca02021-10-14 17:48:25 +02001362 """Check if any of the CardModel sub-classes 'match' the currently inserted card
1363 (by ATR or overriding the 'match' method). If so, call their 'add_files'
1364 method."""
Harald Weltef44256c2021-10-14 15:53:39 +02001365 for m in CardModel.__subclasses__():
1366 if m.match(scc):
1367 m.add_files(rs)