blob: dec521e431b7e0c2aacab29ea3db0b132e9abc60 [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
30
31import cmd2
32from cmd2 import CommandSet, with_default_category, with_argparser
33import argparse
34
Harald Welte1e456572021-04-02 17:16:30 +020035from typing import cast, Optional, Iterable, List, Any, Dict, Tuple
Harald Welteee3501f2021-04-02 13:00:18 +020036
Philipp Maier3aec8712021-03-09 21:49:01 +010037from pySim.utils import sw_match, h2b, b2h, is_hex
Harald Welte2db5cfb2021-04-10 19:05:37 +020038from pySim.construct import filter_dict
Harald Welteb2edd142021-01-08 23:29:35 +010039from pySim.exceptions import *
Harald Welte0d4e98a2021-04-07 00:14:40 +020040from pySim.jsonpath import js_path_find, js_path_modify
Harald Welteb2edd142021-01-08 23:29:35 +010041
42class CardFile(object):
43 """Base class for all objects in the smart card filesystem.
44 Serve as a common ancestor to all other file types; rarely used directly.
45 """
46 RESERVED_NAMES = ['..', '.', '/', 'MF']
47 RESERVED_FIDS = ['3f00']
48
Harald Welteee3501f2021-04-02 13:00:18 +020049 def __init__(self, fid:str=None, sfid:str=None, name:str=None, desc:str=None,
50 parent:Optional['CardDF']=None):
51 """
52 Args:
53 fid : File Identifier (4 hex digits)
54 sfid : Short File Identifier (2 hex digits, optional)
55 name : Brief name of the file, lik EF_ICCID
Harald Weltec9cdce32021-04-11 10:28:28 +020056 desc : Description of the file
Harald Welteee3501f2021-04-02 13:00:18 +020057 parent : Parent CardFile object within filesystem hierarchy
58 """
Harald Welteb2edd142021-01-08 23:29:35 +010059 if not isinstance(self, CardADF) and fid == None:
60 raise ValueError("fid is mandatory")
61 if fid:
62 fid = fid.lower()
63 self.fid = fid # file identifier
64 self.sfid = sfid # short file identifier
65 self.name = name # human readable name
66 self.desc = desc # human readable description
67 self.parent = parent
68 if self.parent and self.parent != self and self.fid:
69 self.parent.add_file(self)
Vadim Yanitskiya0040792021-04-09 22:44:44 +020070 self.shell_commands = [] # type: List[CommandSet]
Harald Welteb2edd142021-01-08 23:29:35 +010071
Philipp Maier66061582021-03-09 21:57:57 +010072 # Note: the basic properties (fid, name, ect.) are verified when
73 # the file is attached to a parent file. See method add_file() in
74 # class Card DF
75
Harald Welteb2edd142021-01-08 23:29:35 +010076 def __str__(self):
77 if self.name:
78 return self.name
79 else:
80 return self.fid
81
Harald Welteee3501f2021-04-02 13:00:18 +020082 def _path_element(self, prefer_name:bool) -> Optional[str]:
Harald Welteb2edd142021-01-08 23:29:35 +010083 if prefer_name and self.name:
84 return self.name
85 else:
86 return self.fid
87
Harald Welte1e456572021-04-02 17:16:30 +020088 def fully_qualified_path(self, prefer_name:bool=True) -> List[str]:
Harald Welteee3501f2021-04-02 13:00:18 +020089 """Return fully qualified path to file as list of FID or name strings.
90
91 Args:
92 prefer_name : Preferably build path of names; fall-back to FIDs as required
93 """
Harald Welte1e456572021-04-02 17:16:30 +020094 if self.parent and self.parent != self:
Harald Welteb2edd142021-01-08 23:29:35 +010095 ret = self.parent.fully_qualified_path(prefer_name)
96 else:
97 ret = []
Harald Welte1e456572021-04-02 17:16:30 +020098 elem = self._path_element(prefer_name)
99 if elem:
100 ret.append(elem)
Harald Welteb2edd142021-01-08 23:29:35 +0100101 return ret
102
Harald Welteee3501f2021-04-02 13:00:18 +0200103 def get_mf(self) -> Optional['CardMF']:
Harald Welteb2edd142021-01-08 23:29:35 +0100104 """Return the MF (root) of the file system."""
105 if self.parent == None:
106 return None
107 # iterate towards the top. MF has parent == self
108 node = self
Harald Welte1e456572021-04-02 17:16:30 +0200109 while node.parent and node.parent != node:
Harald Welteb2edd142021-01-08 23:29:35 +0100110 node = node.parent
Harald Welte1e456572021-04-02 17:16:30 +0200111 return cast(CardMF, node)
Harald Welteb2edd142021-01-08 23:29:35 +0100112
Harald Welte1e456572021-04-02 17:16:30 +0200113 def _get_self_selectables(self, alias:str=None, flags = []) -> Dict[str, 'CardFile']:
Harald Welteee3501f2021-04-02 13:00:18 +0200114 """Return a dict of {'identifier': self} tuples.
115
116 Args:
117 alias : Add an alias with given name to 'self'
118 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
119 If not specified, all selectables will be returned.
120 Returns:
121 dict containing reference to 'self' for all identifiers.
122 """
Harald Welteb2edd142021-01-08 23:29:35 +0100123 sels = {}
124 if alias:
125 sels.update({alias: self})
Philipp Maier786f7812021-02-25 16:48:10 +0100126 if self.fid and (flags == [] or 'FIDS' in flags):
Harald Welteb2edd142021-01-08 23:29:35 +0100127 sels.update({self.fid: self})
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100128 if self.name and (flags == [] or 'FNAMES' in flags):
Harald Welteb2edd142021-01-08 23:29:35 +0100129 sels.update({self.name: self})
130 return sels
131
Harald Welte1e456572021-04-02 17:16:30 +0200132 def get_selectables(self, flags = []) -> Dict[str, 'CardFile']:
Harald Welteee3501f2021-04-02 13:00:18 +0200133 """Return a dict of {'identifier': File} that is selectable from the current file.
134
135 Args:
136 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
137 If not specified, all selectables will be returned.
138 Returns:
139 dict containing all selectable items. Key is identifier (string), value
140 a reference to a CardFile (or derived class) instance.
141 """
Philipp Maier786f7812021-02-25 16:48:10 +0100142 sels = {}
Harald Welteb2edd142021-01-08 23:29:35 +0100143 # we can always select ourself
Philipp Maier786f7812021-02-25 16:48:10 +0100144 if flags == [] or 'SELF' in flags:
145 sels = self._get_self_selectables('.', flags)
Harald Welteb2edd142021-01-08 23:29:35 +0100146 # we can always select our parent
Philipp Maier786f7812021-02-25 16:48:10 +0100147 if flags == [] or 'PARENT' in flags:
Harald Welte1e456572021-04-02 17:16:30 +0200148 if self.parent:
149 sels = self.parent._get_self_selectables('..', flags)
Harald Welteb2edd142021-01-08 23:29:35 +0100150 # if we have a MF, we can always select its applications
Philipp Maier786f7812021-02-25 16:48:10 +0100151 if flags == [] or 'MF' in flags:
152 mf = self.get_mf()
153 if mf:
154 sels.update(mf._get_self_selectables(flags = flags))
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100155 sels.update(mf.get_app_selectables(flags = flags))
Harald Welteb2edd142021-01-08 23:29:35 +0100156 return sels
157
Harald Welte1e456572021-04-02 17:16:30 +0200158 def get_selectable_names(self, flags = []) -> List[str]:
Harald Welteee3501f2021-04-02 13:00:18 +0200159 """Return a dict of {'identifier': File} that is selectable from the current file.
160
161 Args:
162 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
163 If not specified, all selectables will be returned.
164 Returns:
Harald Welte1e456572021-04-02 17:16:30 +0200165 list containing all selectable names.
Harald Welteee3501f2021-04-02 13:00:18 +0200166 """
Philipp Maier786f7812021-02-25 16:48:10 +0100167 sels = self.get_selectables(flags)
Harald Welte1e456572021-04-02 17:16:30 +0200168 return list(sels.keys())
Harald Welteb2edd142021-01-08 23:29:35 +0100169
Harald Welteee3501f2021-04-02 13:00:18 +0200170 def decode_select_response(self, data_hex:str):
Harald Welteb2edd142021-01-08 23:29:35 +0100171 """Decode the response to a SELECT command."""
Harald Welte1e456572021-04-02 17:16:30 +0200172 if self.parent:
173 return self.parent.decode_select_response(data_hex)
Harald Welteb2edd142021-01-08 23:29:35 +0100174
175
176class CardDF(CardFile):
177 """DF (Dedicated File) in the smart card filesystem. Those are basically sub-directories."""
Philipp Maier63f572d2021-03-09 22:42:47 +0100178
179 @with_default_category('DF/ADF Commands')
180 class ShellCommands(CommandSet):
181 def __init__(self):
182 super().__init__()
183
Harald Welteb2edd142021-01-08 23:29:35 +0100184 def __init__(self, **kwargs):
185 if not isinstance(self, CardADF):
186 if not 'fid' in kwargs:
187 raise TypeError('fid is mandatory for all DF')
188 super().__init__(**kwargs)
189 self.children = dict()
Philipp Maier63f572d2021-03-09 22:42:47 +0100190 self.shell_commands = [self.ShellCommands()]
Harald Welteb2edd142021-01-08 23:29:35 +0100191
192 def __str__(self):
193 return "DF(%s)" % (super().__str__())
194
Harald Welteee3501f2021-04-02 13:00:18 +0200195 def add_file(self, child:CardFile, ignore_existing:bool=False):
196 """Add a child (DF/EF) to this DF.
197 Args:
198 child: The new DF/EF to be added
199 ignore_existing: Ignore, if file with given FID already exists. Old one will be kept.
200 """
Harald Welteb2edd142021-01-08 23:29:35 +0100201 if not isinstance(child, CardFile):
202 raise TypeError("Expected a File instance")
Philipp Maier3aec8712021-03-09 21:49:01 +0100203 if not is_hex(child.fid, minlen = 4, maxlen = 4):
204 raise ValueError("File name %s is not a valid fid" % (child.fid))
Harald Welteb2edd142021-01-08 23:29:35 +0100205 if child.name in CardFile.RESERVED_NAMES:
206 raise ValueError("File name %s is a reserved name" % (child.name))
207 if child.fid in CardFile.RESERVED_FIDS:
Philipp Maiere8bc1b42021-03-09 20:33:41 +0100208 raise ValueError("File fid %s is a reserved fid" % (child.fid))
Harald Welteb2edd142021-01-08 23:29:35 +0100209 if child.fid in self.children:
210 if ignore_existing:
211 return
Harald Welte977035c2021-04-21 11:01:26 +0200212 raise ValueError("File with given fid %s already exists in %s" % (child.fid, self))
Harald Welteb2edd142021-01-08 23:29:35 +0100213 if self.lookup_file_by_sfid(child.sfid):
Harald Welte977035c2021-04-21 11:01:26 +0200214 raise ValueError("File with given sfid %s already exists in %s" % (child.sfid, self))
Harald Welteb2edd142021-01-08 23:29:35 +0100215 if self.lookup_file_by_name(child.name):
216 if ignore_existing:
217 return
Harald Welte977035c2021-04-21 11:01:26 +0200218 raise ValueError("File with given name %s already exists in %s" % (child.name, self))
Harald Welteb2edd142021-01-08 23:29:35 +0100219 self.children[child.fid] = child
220 child.parent = self
221
Harald Welteee3501f2021-04-02 13:00:18 +0200222 def add_files(self, children:Iterable[CardFile], ignore_existing:bool=False):
223 """Add a list of child (DF/EF) to this DF
224
225 Args:
226 children: List of new DF/EFs to be added
227 ignore_existing: Ignore, if file[s] with given FID already exists. Old one[s] will be kept.
228 """
Harald Welteb2edd142021-01-08 23:29:35 +0100229 for child in children:
230 self.add_file(child, ignore_existing)
231
Harald Welteee3501f2021-04-02 13:00:18 +0200232 def get_selectables(self, flags = []) -> dict:
233 """Return a dict of {'identifier': File} that is selectable from the current DF.
234
235 Args:
236 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
237 If not specified, all selectables will be returned.
238 Returns:
239 dict containing all selectable items. Key is identifier (string), value
240 a reference to a CardFile (or derived class) instance.
241 """
Harald Welteb2edd142021-01-08 23:29:35 +0100242 # global selectables + our children
Philipp Maier786f7812021-02-25 16:48:10 +0100243 sels = super().get_selectables(flags)
244 if flags == [] or 'FIDS' in flags:
245 sels.update({x.fid: x for x in self.children.values() if x.fid})
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100246 if flags == [] or 'FNAMES' in flags:
Philipp Maier786f7812021-02-25 16:48:10 +0100247 sels.update({x.name: x for x in self.children.values() if x.name})
Harald Welteb2edd142021-01-08 23:29:35 +0100248 return sels
249
Harald Welte1e456572021-04-02 17:16:30 +0200250 def lookup_file_by_name(self, name:Optional[str]) -> Optional[CardFile]:
Harald Welteee3501f2021-04-02 13:00:18 +0200251 """Find a file with given name within current DF."""
Harald Welteb2edd142021-01-08 23:29:35 +0100252 if name == None:
253 return None
254 for i in self.children.values():
255 if i.name and i.name == name:
256 return i
257 return None
258
Harald Welte1e456572021-04-02 17:16:30 +0200259 def lookup_file_by_sfid(self, sfid:Optional[str]) -> Optional[CardFile]:
Harald Welteee3501f2021-04-02 13:00:18 +0200260 """Find a file with given short file ID within current DF."""
Harald Welteb2edd142021-01-08 23:29:35 +0100261 if sfid == None:
262 return None
263 for i in self.children.values():
Harald Welte1e456572021-04-02 17:16:30 +0200264 if i.sfid == int(str(sfid)):
Harald Welteb2edd142021-01-08 23:29:35 +0100265 return i
266 return None
267
Harald Welteee3501f2021-04-02 13:00:18 +0200268 def lookup_file_by_fid(self, fid:str) -> Optional[CardFile]:
269 """Find a file with given file ID within current DF."""
Harald Welteb2edd142021-01-08 23:29:35 +0100270 if fid in self.children:
271 return self.children[fid]
272 return None
273
274
275class CardMF(CardDF):
276 """MF (Master File) in the smart card filesystem"""
277 def __init__(self, **kwargs):
278 # can be overridden; use setdefault
279 kwargs.setdefault('fid', '3f00')
280 kwargs.setdefault('name', 'MF')
281 kwargs.setdefault('desc', 'Master File (directory root)')
282 # cannot be overridden; use assignment
283 kwargs['parent'] = self
284 super().__init__(**kwargs)
285 self.applications = dict()
286
287 def __str__(self):
288 return "MF(%s)" % (self.fid)
289
Harald Welte5ce35242021-04-02 20:27:05 +0200290 def add_application_df(self, app:'CardADF'):
291 """Add an Application to the MF"""
Harald Welteb2edd142021-01-08 23:29:35 +0100292 if not isinstance(app, CardADF):
293 raise TypeError("Expected an ADF instance")
294 if app.aid in self.applications:
295 raise ValueError("AID %s already exists" % (app.aid))
296 self.applications[app.aid] = app
297 app.parent=self
298
299 def get_app_names(self):
300 """Get list of completions (AID names)"""
301 return [x.name for x in self.applications]
302
Harald Welteee3501f2021-04-02 13:00:18 +0200303 def get_selectables(self, flags = []) -> dict:
304 """Return a dict of {'identifier': File} that is selectable from the current DF.
305
306 Args:
307 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
308 If not specified, all selectables will be returned.
309 Returns:
310 dict containing all selectable items. Key is identifier (string), value
311 a reference to a CardFile (or derived class) instance.
312 """
Philipp Maier786f7812021-02-25 16:48:10 +0100313 sels = super().get_selectables(flags)
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100314 sels.update(self.get_app_selectables(flags))
Harald Welteb2edd142021-01-08 23:29:35 +0100315 return sels
316
Harald Welteee3501f2021-04-02 13:00:18 +0200317 def get_app_selectables(self, flags = []) -> dict:
Philipp Maier786f7812021-02-25 16:48:10 +0100318 """Get applications by AID + name"""
319 sels = {}
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100320 if flags == [] or 'AIDS' in flags:
Philipp Maier786f7812021-02-25 16:48:10 +0100321 sels.update({x.aid: x for x in self.applications.values()})
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100322 if flags == [] or 'ANAMES' in flags:
Philipp Maier786f7812021-02-25 16:48:10 +0100323 sels.update({x.name: x for x in self.applications.values() if x.name})
Harald Welteb2edd142021-01-08 23:29:35 +0100324 return sels
325
Harald Welteee3501f2021-04-02 13:00:18 +0200326 def decode_select_response(self, data_hex:str) -> Any:
327 """Decode the response to a SELECT command.
328
329 This is the fall-back method which doesn't perform any decoding. It mostly
330 exists so specific derived classes can overload it for actual decoding.
331 """
Harald Welteb2edd142021-01-08 23:29:35 +0100332 return data_hex
333
334
335
336class CardADF(CardDF):
337 """ADF (Application Dedicated File) in the smart card filesystem"""
Harald Welteee3501f2021-04-02 13:00:18 +0200338 def __init__(self, aid:str, **kwargs):
Harald Welteb2edd142021-01-08 23:29:35 +0100339 super().__init__(**kwargs)
Harald Welte5ce35242021-04-02 20:27:05 +0200340 # reference to CardApplication may be set from CardApplication constructor
Harald Weltefe8a7442021-04-10 11:51:54 +0200341 self.application = None # type: Optional[CardApplication]
Harald Welteb2edd142021-01-08 23:29:35 +0100342 self.aid = aid # Application Identifier
Harald Welte1e456572021-04-02 17:16:30 +0200343 mf = self.get_mf()
344 if mf:
Harald Welte5ce35242021-04-02 20:27:05 +0200345 mf.add_application_df(self)
Harald Welteb2edd142021-01-08 23:29:35 +0100346
347 def __str__(self):
348 return "ADF(%s)" % (self.aid)
349
Harald Welteee3501f2021-04-02 13:00:18 +0200350 def _path_element(self, prefer_name:bool):
Harald Welteb2edd142021-01-08 23:29:35 +0100351 if self.name and prefer_name:
352 return self.name
353 else:
354 return self.aid
355
356
357class CardEF(CardFile):
358 """EF (Entry File) in the smart card filesystem"""
359 def __init__(self, *, fid, **kwargs):
360 kwargs['fid'] = fid
361 super().__init__(**kwargs)
362
363 def __str__(self):
364 return "EF(%s)" % (super().__str__())
365
Harald Welteee3501f2021-04-02 13:00:18 +0200366 def get_selectables(self, flags = []) -> dict:
367 """Return a dict of {'identifier': File} that is selectable from the current DF.
368
369 Args:
370 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
371 If not specified, all selectables will be returned.
372 Returns:
373 dict containing all selectable items. Key is identifier (string), value
374 a reference to a CardFile (or derived class) instance.
375 """
Harald Welteb2edd142021-01-08 23:29:35 +0100376 #global selectable names + those of the parent DF
Philipp Maier786f7812021-02-25 16:48:10 +0100377 sels = super().get_selectables(flags)
Harald Welteb2edd142021-01-08 23:29:35 +0100378 sels.update({x.name:x for x in self.parent.children.values() if x != self})
379 return sels
380
381
382class TransparentEF(CardEF):
Harald Welteee3501f2021-04-02 13:00:18 +0200383 """Transparent EF (Entry File) in the smart card filesystem.
384
385 A Transparent EF is a binary file with no formal structure. This is contrary to
386 Record based EFs which have [fixed size] records that can be individually read/updated."""
Harald Welteb2edd142021-01-08 23:29:35 +0100387
388 @with_default_category('Transparent EF Commands')
389 class ShellCommands(CommandSet):
Harald Weltec9cdce32021-04-11 10:28:28 +0200390 """Shell commands specific for transparent EFs."""
Harald Welteb2edd142021-01-08 23:29:35 +0100391 def __init__(self):
392 super().__init__()
393
394 read_bin_parser = argparse.ArgumentParser()
395 read_bin_parser.add_argument('--offset', type=int, default=0, help='Byte offset for start of read')
396 read_bin_parser.add_argument('--length', type=int, help='Number of bytes to read')
397 @cmd2.with_argparser(read_bin_parser)
398 def do_read_binary(self, opts):
399 """Read binary data from a transparent EF"""
400 (data, sw) = self._cmd.rs.read_binary(opts.length, opts.offset)
401 self._cmd.poutput(data)
402
Harald Weltebcad86c2021-04-06 20:08:39 +0200403 read_bin_dec_parser = argparse.ArgumentParser()
404 read_bin_dec_parser.add_argument('--oneline', action='store_true',
405 help='No JSON pretty-printing, dump as a single line')
406 @cmd2.with_argparser(read_bin_dec_parser)
Harald Welteb2edd142021-01-08 23:29:35 +0100407 def do_read_binary_decoded(self, opts):
408 """Read + decode data from a transparent EF"""
409 (data, sw) = self._cmd.rs.read_binary_dec()
Harald Welte1748b932021-04-06 21:12:25 +0200410 self._cmd.poutput_json(data, opts.oneline)
Harald Welteb2edd142021-01-08 23:29:35 +0100411
412 upd_bin_parser = argparse.ArgumentParser()
413 upd_bin_parser.add_argument('--offset', type=int, default=0, help='Byte offset for start of read')
414 upd_bin_parser.add_argument('data', help='Data bytes (hex format) to write')
415 @cmd2.with_argparser(upd_bin_parser)
416 def do_update_binary(self, opts):
417 """Update (Write) data of a transparent EF"""
418 (data, sw) = self._cmd.rs.update_binary(opts.data, opts.offset)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100419 if data:
420 self._cmd.poutput(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100421
422 upd_bin_dec_parser = argparse.ArgumentParser()
423 upd_bin_dec_parser.add_argument('data', help='Abstract data (JSON format) to write')
Harald Welte0d4e98a2021-04-07 00:14:40 +0200424 upd_bin_dec_parser.add_argument('--json-path', type=str,
425 help='JSON path to modify specific element of file only')
Harald Welteb2edd142021-01-08 23:29:35 +0100426 @cmd2.with_argparser(upd_bin_dec_parser)
427 def do_update_binary_decoded(self, opts):
428 """Encode + Update (Write) data of a transparent EF"""
Harald Welte0d4e98a2021-04-07 00:14:40 +0200429 if opts.json_path:
430 (data_json, sw) = self._cmd.rs.read_binary_dec()
431 js_path_modify(data_json, opts.json_path, json.loads(opts.data))
432 else:
433 data_json = json.loads(opts.data)
Harald Welteb2edd142021-01-08 23:29:35 +0100434 (data, sw) = self._cmd.rs.update_binary_dec(data_json)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100435 if data:
Harald Welte1748b932021-04-06 21:12:25 +0200436 self._cmd.poutput_json(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100437
Harald Welte4145d3c2021-04-08 20:34:13 +0200438 def do_edit_binary_decoded(self, opts):
439 """Edit the JSON representation of the EF contents in an editor."""
440 (orig_json, sw) = self._cmd.rs.read_binary_dec()
441 with tempfile.TemporaryDirectory(prefix='pysim_') as dirname:
442 filename = '%s/file' % dirname
443 # write existing data as JSON to file
444 with open(filename, 'w') as text_file:
445 json.dump(orig_json, text_file, indent=4)
446 # run a text editor
447 self._cmd._run_editor(filename)
448 with open(filename, 'r') as text_file:
449 edited_json = json.load(text_file)
450 if edited_json == orig_json:
451 self._cmd.poutput("Data not modified, skipping write")
452 else:
453 (data, sw) = self._cmd.rs.update_binary_dec(edited_json)
454 if data:
455 self._cmd.poutput_json(data)
456
457
Harald Welteee3501f2021-04-02 13:00:18 +0200458 def __init__(self, fid:str, sfid:str=None, name:str=None, desc:str=None, parent:CardDF=None,
459 size={1,None}):
460 """
461 Args:
462 fid : File Identifier (4 hex digits)
463 sfid : Short File Identifier (2 hex digits, optional)
464 name : Brief name of the file, lik EF_ICCID
Harald Weltec9cdce32021-04-11 10:28:28 +0200465 desc : Description of the file
Harald Welteee3501f2021-04-02 13:00:18 +0200466 parent : Parent CardFile object within filesystem hierarchy
467 size : tuple of (minimum_size, recommended_size)
468 """
Harald Welteb2edd142021-01-08 23:29:35 +0100469 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200470 self._construct = None
Harald Welteb2edd142021-01-08 23:29:35 +0100471 self.size = size
472 self.shell_commands = [self.ShellCommands()]
473
Harald Welteee3501f2021-04-02 13:00:18 +0200474 def decode_bin(self, raw_bin_data:bytearray) -> dict:
475 """Decode raw (binary) data into abstract representation.
476
477 A derived class would typically provide a _decode_bin() or _decode_hex() method
478 for implementing this specifically for the given file. This function checks which
479 of the method exists, add calls them (with conversion, as needed).
480
481 Args:
482 raw_bin_data : binary encoded data
483 Returns:
484 abstract_data; dict representing the decoded data
485 """
Harald Welteb2edd142021-01-08 23:29:35 +0100486 method = getattr(self, '_decode_bin', None)
487 if callable(method):
488 return method(raw_bin_data)
489 method = getattr(self, '_decode_hex', None)
490 if callable(method):
491 return method(b2h(raw_bin_data))
Harald Welte2db5cfb2021-04-10 19:05:37 +0200492 if self._construct:
493 return filter_dict(self._construct.parse(raw_bin_data, total_len=len(raw_bin_data)))
Harald Welteb2edd142021-01-08 23:29:35 +0100494 return {'raw': raw_bin_data.hex()}
495
Harald Welteee3501f2021-04-02 13:00:18 +0200496 def decode_hex(self, raw_hex_data:str) -> dict:
497 """Decode raw (hex string) data into abstract representation.
498
499 A derived class would typically provide a _decode_bin() or _decode_hex() method
500 for implementing this specifically for the given file. This function checks which
501 of the method exists, add calls them (with conversion, as needed).
502
503 Args:
504 raw_hex_data : hex-encoded data
505 Returns:
506 abstract_data; dict representing the decoded data
507 """
Harald Welteb2edd142021-01-08 23:29:35 +0100508 method = getattr(self, '_decode_hex', None)
509 if callable(method):
510 return method(raw_hex_data)
511 raw_bin_data = h2b(raw_hex_data)
512 method = getattr(self, '_decode_bin', None)
513 if callable(method):
514 return method(raw_bin_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200515 if self._construct:
516 return filter_dict(self._construct.parse(raw_bin_data, total_len=len(raw_bin_data)))
Harald Welteb2edd142021-01-08 23:29:35 +0100517 return {'raw': raw_bin_data.hex()}
518
Harald Welteee3501f2021-04-02 13:00:18 +0200519 def encode_bin(self, abstract_data:dict) -> bytearray:
520 """Encode abstract representation into raw (binary) data.
521
522 A derived class would typically provide an _encode_bin() or _encode_hex() method
523 for implementing this specifically for the given file. This function checks which
524 of the method exists, add calls them (with conversion, as needed).
525
526 Args:
527 abstract_data : dict representing the decoded data
528 Returns:
529 binary encoded data
530 """
Harald Welteb2edd142021-01-08 23:29:35 +0100531 method = getattr(self, '_encode_bin', None)
532 if callable(method):
533 return method(abstract_data)
534 method = getattr(self, '_encode_hex', None)
535 if callable(method):
536 return h2b(method(abstract_data))
Harald Welte2db5cfb2021-04-10 19:05:37 +0200537 if self._construct:
538 return self._construct.build(abstract_data)
Harald Welteb2edd142021-01-08 23:29:35 +0100539 raise NotImplementedError
540
Harald Welteee3501f2021-04-02 13:00:18 +0200541 def encode_hex(self, abstract_data:dict) -> str:
542 """Encode abstract representation into raw (hex string) data.
543
544 A derived class would typically provide an _encode_bin() or _encode_hex() method
545 for implementing this specifically for the given file. This function checks which
546 of the method exists, add calls them (with conversion, as needed).
547
548 Args:
549 abstract_data : dict representing the decoded data
550 Returns:
551 hex string encoded data
552 """
Harald Welteb2edd142021-01-08 23:29:35 +0100553 method = getattr(self, '_encode_hex', None)
554 if callable(method):
555 return method(abstract_data)
556 method = getattr(self, '_encode_bin', None)
557 if callable(method):
558 raw_bin_data = method(abstract_data)
559 return b2h(raw_bin_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200560 if self._construct:
561 return b2h(self._construct.build(abstract_data))
Harald Welteb2edd142021-01-08 23:29:35 +0100562 raise NotImplementedError
563
564
565class LinFixedEF(CardEF):
Harald Welteee3501f2021-04-02 13:00:18 +0200566 """Linear Fixed EF (Entry File) in the smart card filesystem.
567
568 Linear Fixed EFs are record oriented files. They consist of a number of fixed-size
569 records. The records can be individually read/updated."""
Harald Welteb2edd142021-01-08 23:29:35 +0100570
571 @with_default_category('Linear Fixed EF Commands')
572 class ShellCommands(CommandSet):
Harald Welteee3501f2021-04-02 13:00:18 +0200573 """Shell commands specific for Linear Fixed EFs."""
Harald Welteb2edd142021-01-08 23:29:35 +0100574 def __init__(self):
575 super().__init__()
576
577 read_rec_parser = argparse.ArgumentParser()
578 read_rec_parser.add_argument('record_nr', type=int, help='Number of record to be read')
Philipp Maier41555732021-02-25 16:52:08 +0100579 read_rec_parser.add_argument('--count', type=int, default=1, help='Number of records to be read, beginning at record_nr')
Harald Welteb2edd142021-01-08 23:29:35 +0100580 @cmd2.with_argparser(read_rec_parser)
581 def do_read_record(self, opts):
Philipp Maier41555732021-02-25 16:52:08 +0100582 """Read one or multiple records from a record-oriented EF"""
583 for r in range(opts.count):
584 recnr = opts.record_nr + r
585 (data, sw) = self._cmd.rs.read_record(recnr)
586 if (len(data) > 0):
587 recstr = str(data)
588 else:
589 recstr = "(empty)"
590 self._cmd.poutput("%03d %s" % (recnr, recstr))
Harald Welteb2edd142021-01-08 23:29:35 +0100591
592 read_rec_dec_parser = argparse.ArgumentParser()
593 read_rec_dec_parser.add_argument('record_nr', type=int, help='Number of record to be read')
Harald Weltebcad86c2021-04-06 20:08:39 +0200594 read_rec_dec_parser.add_argument('--oneline', action='store_true',
595 help='No JSON pretty-printing, dump as a single line')
Harald Welteb2edd142021-01-08 23:29:35 +0100596 @cmd2.with_argparser(read_rec_dec_parser)
597 def do_read_record_decoded(self, opts):
598 """Read + decode a record from a record-oriented EF"""
599 (data, sw) = self._cmd.rs.read_record_dec(opts.record_nr)
Harald Welte1748b932021-04-06 21:12:25 +0200600 self._cmd.poutput_json(data, opts.oneline)
Harald Welteb2edd142021-01-08 23:29:35 +0100601
Harald Welte850b72a2021-04-07 09:33:03 +0200602 read_recs_parser = argparse.ArgumentParser()
603 @cmd2.with_argparser(read_recs_parser)
604 def do_read_records(self, opts):
605 """Read all records from a record-oriented EF"""
606 num_of_rec = self._cmd.rs.selected_file_fcp['file_descriptor']['num_of_rec']
607 for recnr in range(1, 1 + num_of_rec):
608 (data, sw) = self._cmd.rs.read_record(recnr)
609 if (len(data) > 0):
610 recstr = str(data)
611 else:
612 recstr = "(empty)"
613 self._cmd.poutput("%03d %s" % (recnr, recstr))
614
615 read_recs_dec_parser = argparse.ArgumentParser()
616 read_recs_dec_parser.add_argument('--oneline', action='store_true',
617 help='No JSON pretty-printing, dump as a single line')
618 @cmd2.with_argparser(read_recs_dec_parser)
619 def do_read_records_decoded(self, opts):
620 """Read + decode all records from a record-oriented EF"""
621 num_of_rec = self._cmd.rs.selected_file_fcp['file_descriptor']['num_of_rec']
622 # collect all results in list so they are rendered as JSON list when printing
623 data_list = []
624 for recnr in range(1, 1 + num_of_rec):
625 (data, sw) = self._cmd.rs.read_record_dec(recnr)
626 data_list.append(data)
627 self._cmd.poutput_json(data_list, opts.oneline)
628
Harald Welteb2edd142021-01-08 23:29:35 +0100629 upd_rec_parser = argparse.ArgumentParser()
630 upd_rec_parser.add_argument('record_nr', type=int, help='Number of record to be read')
631 upd_rec_parser.add_argument('data', help='Data bytes (hex format) to write')
632 @cmd2.with_argparser(upd_rec_parser)
633 def do_update_record(self, opts):
634 """Update (write) data to a record-oriented EF"""
635 (data, sw) = self._cmd.rs.update_record(opts.record_nr, opts.data)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100636 if data:
637 self._cmd.poutput(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100638
639 upd_rec_dec_parser = argparse.ArgumentParser()
640 upd_rec_dec_parser.add_argument('record_nr', type=int, help='Number of record to be read')
Philipp Maierfe1fb032021-04-20 22:33:12 +0200641 upd_rec_dec_parser.add_argument('data', help='Abstract data (JSON format) to write')
Harald Welte0d4e98a2021-04-07 00:14:40 +0200642 upd_rec_dec_parser.add_argument('--json-path', type=str,
643 help='JSON path to modify specific element of record only')
Harald Welteb2edd142021-01-08 23:29:35 +0100644 @cmd2.with_argparser(upd_rec_dec_parser)
645 def do_update_record_decoded(self, opts):
646 """Encode + Update (write) data to a record-oriented EF"""
Harald Welte0d4e98a2021-04-07 00:14:40 +0200647 if opts.json_path:
648 (data_json, sw) = self._cmd.rs.read_record_dec(opts.record_nr)
649 js_path_modify(data_json, opts.json_path, json.loads(opts.data))
650 else:
651 data_json = json.loads(opts.data)
652 (data, sw) = self._cmd.rs.update_record_dec(opts.record_nr, data_json)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100653 if data:
654 self._cmd.poutput(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100655
Harald Welte4145d3c2021-04-08 20:34:13 +0200656 edit_rec_dec_parser = argparse.ArgumentParser()
657 edit_rec_dec_parser.add_argument('record_nr', type=int, help='Number of record to be edited')
658 @cmd2.with_argparser(edit_rec_dec_parser)
659 def do_edit_record_decoded(self, opts):
660 """Edit the JSON representation of one record in an editor."""
661 (orig_json, sw) = self._cmd.rs.read_record_dec(opts.record_nr)
662 dirname = tempfile.mkdtemp(prefix='pysim_')
663 try:
664 filename = '%s/file' % dirname
665 # write existing data as JSON to file
666 with open(filename, 'w') as text_file:
667 json.dump(orig_json, text_file, indent=4)
668 # run a text editor
669 self._cmd._run_editor(filename)
670 with open(filename, 'r') as text_file:
671 edited_json = json.load(text_file)
672 if edited_json == orig_json:
673 self._cmd.poutput("Data not modified, skipping write")
674 else:
675 (data, sw) = self._cmd.rs.update_record_dec(opts.record_nr, edited_json)
676 if data:
677 self._cmd.poutput_json(data)
678 finally:
679 shutil.rmtree(dirname)
680
681
Harald Welteee3501f2021-04-02 13:00:18 +0200682 def __init__(self, fid:str, sfid:str=None, name:str=None, desc:str=None,
683 parent:Optional[CardDF]=None, rec_len={1,None}):
684 """
685 Args:
686 fid : File Identifier (4 hex digits)
687 sfid : Short File Identifier (2 hex digits, optional)
688 name : Brief name of the file, lik EF_ICCID
Harald Weltec9cdce32021-04-11 10:28:28 +0200689 desc : Description of the file
Harald Welteee3501f2021-04-02 13:00:18 +0200690 parent : Parent CardFile object within filesystem hierarchy
Philipp Maier0adabf62021-04-20 22:36:41 +0200691 rec_len : set of {minimum_length, recommended_length}
Harald Welteee3501f2021-04-02 13:00:18 +0200692 """
Harald Welteb2edd142021-01-08 23:29:35 +0100693 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent)
694 self.rec_len = rec_len
695 self.shell_commands = [self.ShellCommands()]
Harald Welte2db5cfb2021-04-10 19:05:37 +0200696 self._construct = None
Harald Welteb2edd142021-01-08 23:29:35 +0100697
Harald Welteee3501f2021-04-02 13:00:18 +0200698 def decode_record_hex(self, raw_hex_data:str) -> dict:
699 """Decode raw (hex string) data into abstract representation.
700
701 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
702 method for implementing this specifically for the given file. This function checks which
703 of the method exists, add calls them (with conversion, as needed).
704
705 Args:
706 raw_hex_data : hex-encoded data
707 Returns:
708 abstract_data; dict representing the decoded data
709 """
Harald Welteb2edd142021-01-08 23:29:35 +0100710 method = getattr(self, '_decode_record_hex', None)
711 if callable(method):
712 return method(raw_hex_data)
713 raw_bin_data = h2b(raw_hex_data)
714 method = getattr(self, '_decode_record_bin', None)
715 if callable(method):
716 return method(raw_bin_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200717 if self._construct:
718 return filter_dict(self._construct.parse(raw_bin_data, total_len=len(raw_bin_data)))
Harald Welteb2edd142021-01-08 23:29:35 +0100719 return {'raw': raw_bin_data.hex()}
720
Harald Welteee3501f2021-04-02 13:00:18 +0200721 def decode_record_bin(self, raw_bin_data:bytearray) -> dict:
722 """Decode raw (binary) data into abstract representation.
723
724 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
725 method for implementing this specifically for the given file. This function checks which
726 of the method exists, add calls them (with conversion, as needed).
727
728 Args:
729 raw_bin_data : binary encoded data
730 Returns:
731 abstract_data; dict representing the decoded data
732 """
Harald Welteb2edd142021-01-08 23:29:35 +0100733 method = getattr(self, '_decode_record_bin', None)
734 if callable(method):
735 return method(raw_bin_data)
736 raw_hex_data = b2h(raw_bin_data)
737 method = getattr(self, '_decode_record_hex', None)
738 if callable(method):
739 return method(raw_hex_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200740 if self._construct:
741 return filter_dict(self._construct.parse(raw_bin_data, total_len=len(raw_bin_data)))
Harald Welteb2edd142021-01-08 23:29:35 +0100742 return {'raw': raw_hex_data}
743
Harald Welteee3501f2021-04-02 13:00:18 +0200744 def encode_record_hex(self, abstract_data:dict) -> str:
745 """Encode abstract representation into raw (hex string) data.
746
747 A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
748 method for implementing this specifically for the given file. This function checks which
749 of the method exists, add calls them (with conversion, as needed).
750
751 Args:
752 abstract_data : dict representing the decoded data
753 Returns:
754 hex string encoded data
755 """
Harald Welteb2edd142021-01-08 23:29:35 +0100756 method = getattr(self, '_encode_record_hex', None)
757 if callable(method):
758 return method(abstract_data)
759 method = getattr(self, '_encode_record_bin', None)
760 if callable(method):
761 raw_bin_data = method(abstract_data)
Harald Welte1e456572021-04-02 17:16:30 +0200762 return b2h(raw_bin_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200763 if self._construct:
764 return b2h(self._construct.build(abstract_data))
Harald Welteb2edd142021-01-08 23:29:35 +0100765 raise NotImplementedError
766
Harald Welteee3501f2021-04-02 13:00:18 +0200767 def encode_record_bin(self, abstract_data:dict) -> bytearray:
768 """Encode abstract representation into raw (binary) data.
769
770 A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
771 method for implementing this specifically for the given file. This function checks which
772 of the method exists, add calls them (with conversion, as needed).
773
774 Args:
775 abstract_data : dict representing the decoded data
776 Returns:
777 binary encoded data
778 """
Harald Welteb2edd142021-01-08 23:29:35 +0100779 method = getattr(self, '_encode_record_bin', None)
780 if callable(method):
781 return method(abstract_data)
782 method = getattr(self, '_encode_record_hex', None)
783 if callable(method):
Harald Welteee3501f2021-04-02 13:00:18 +0200784 return h2b(method(abstract_data))
Harald Welte2db5cfb2021-04-10 19:05:37 +0200785 if self._construct:
786 return self._construct.build(abstract_data)
Harald Welteb2edd142021-01-08 23:29:35 +0100787 raise NotImplementedError
788
789class CyclicEF(LinFixedEF):
790 """Cyclic EF (Entry File) in the smart card filesystem"""
791 # we don't really have any special support for those; just recycling LinFixedEF here
Harald Welteee3501f2021-04-02 13:00:18 +0200792 def __init__(self, fid:str, sfid:str=None, name:str=None, desc:str=None, parent:CardDF=None,
793 rec_len={1,None}):
Harald Welteb2edd142021-01-08 23:29:35 +0100794 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, rec_len=rec_len)
795
796class TransRecEF(TransparentEF):
797 """Transparent EF (Entry File) containing fixed-size records.
Harald Welteee3501f2021-04-02 13:00:18 +0200798
Harald Welteb2edd142021-01-08 23:29:35 +0100799 These are the real odd-balls and mostly look like mistakes in the specification:
800 Specified as 'transparent' EF, but actually containing several fixed-length records
801 inside.
802 We add a special class for those, so the user only has to provide encoder/decoder functions
803 for a record, while this class takes care of split / merge of records.
804 """
Harald Welte1e456572021-04-02 17:16:30 +0200805 def __init__(self, fid:str, rec_len:int, sfid:str=None, name:str=None, desc:str=None,
806 parent:Optional[CardDF]=None, size={1,None}):
Harald Welteee3501f2021-04-02 13:00:18 +0200807 """
808 Args:
809 fid : File Identifier (4 hex digits)
810 sfid : Short File Identifier (2 hex digits, optional)
Harald Weltec9cdce32021-04-11 10:28:28 +0200811 name : Brief name of the file, like EF_ICCID
812 desc : Description of the file
Harald Welteee3501f2021-04-02 13:00:18 +0200813 parent : Parent CardFile object within filesystem hierarchy
814 rec_len : Length of the fixed-length records within transparent EF
815 size : tuple of (minimum_size, recommended_size)
816 """
Harald Welteb2edd142021-01-08 23:29:35 +0100817 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, size=size)
818 self.rec_len = rec_len
819
Harald Welteee3501f2021-04-02 13:00:18 +0200820 def decode_record_hex(self, raw_hex_data:str) -> dict:
821 """Decode raw (hex string) data into abstract representation.
822
823 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
824 method for implementing this specifically for the given file. This function checks which
825 of the method exists, add calls them (with conversion, as needed).
826
827 Args:
828 raw_hex_data : hex-encoded data
829 Returns:
830 abstract_data; dict representing the decoded data
831 """
Harald Welteb2edd142021-01-08 23:29:35 +0100832 method = getattr(self, '_decode_record_hex', None)
833 if callable(method):
834 return method(raw_hex_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200835 raw_bin_data = h2b(raw_hex_data)
Harald Welteb2edd142021-01-08 23:29:35 +0100836 method = getattr(self, '_decode_record_bin', None)
837 if callable(method):
Harald Welteb2edd142021-01-08 23:29:35 +0100838 return method(raw_bin_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200839 if self._construct:
840 return filter_dict(self._construct.parse(raw_bin_data, total_len=len(raw_bin_data)))
Harald Welteb2edd142021-01-08 23:29:35 +0100841 return {'raw': raw_hex_data}
842
Harald Welteee3501f2021-04-02 13:00:18 +0200843 def decode_record_bin(self, raw_bin_data:bytearray) -> dict:
844 """Decode raw (binary) data into abstract representation.
845
846 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
847 method for implementing this specifically for the given file. This function checks which
848 of the method exists, add calls them (with conversion, as needed).
849
850 Args:
851 raw_bin_data : binary encoded data
852 Returns:
853 abstract_data; dict representing the decoded data
854 """
Harald Welteb2edd142021-01-08 23:29:35 +0100855 method = getattr(self, '_decode_record_bin', None)
856 if callable(method):
857 return method(raw_bin_data)
858 raw_hex_data = b2h(raw_bin_data)
859 method = getattr(self, '_decode_record_hex', None)
860 if callable(method):
861 return method(raw_hex_data)
Harald Welte2db5cfb2021-04-10 19:05:37 +0200862 if self._construct:
863 return filter_dict(self._construct.parse(raw_bin_data, total_len=len(raw_bin_data)))
Harald Welteb2edd142021-01-08 23:29:35 +0100864 return {'raw': raw_hex_data}
865
Harald Welteee3501f2021-04-02 13:00:18 +0200866 def encode_record_hex(self, abstract_data:dict) -> str:
867 """Encode abstract representation into raw (hex string) data.
868
869 A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
870 method for implementing this specifically for the given file. This function checks which
871 of the method exists, add calls them (with conversion, as needed).
872
873 Args:
874 abstract_data : dict representing the decoded data
875 Returns:
876 hex string encoded data
877 """
Harald Welteb2edd142021-01-08 23:29:35 +0100878 method = getattr(self, '_encode_record_hex', None)
879 if callable(method):
880 return method(abstract_data)
881 method = getattr(self, '_encode_record_bin', None)
882 if callable(method):
Harald Welte1e456572021-04-02 17:16:30 +0200883 return b2h(method(abstract_data))
Harald Welte2db5cfb2021-04-10 19:05:37 +0200884 if self._construct:
885 return b2h(filter_dict(self._construct.build(abstract_data)))
Harald Welteb2edd142021-01-08 23:29:35 +0100886 raise NotImplementedError
887
Harald Welteee3501f2021-04-02 13:00:18 +0200888 def encode_record_bin(self, abstract_data:dict) -> bytearray:
889 """Encode abstract representation into raw (binary) data.
890
891 A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
892 method for implementing this specifically for the given file. This function checks which
893 of the method exists, add calls them (with conversion, as needed).
894
895 Args:
896 abstract_data : dict representing the decoded data
897 Returns:
898 binary encoded data
899 """
Harald Welteb2edd142021-01-08 23:29:35 +0100900 method = getattr(self, '_encode_record_bin', None)
901 if callable(method):
902 return method(abstract_data)
903 method = getattr(self, '_encode_record_hex', None)
904 if callable(method):
905 return h2b(method(abstract_data))
Harald Welte2db5cfb2021-04-10 19:05:37 +0200906 if self._construct:
907 return filter_dict(self._construct.build(abstract_data))
Harald Welteb2edd142021-01-08 23:29:35 +0100908 raise NotImplementedError
909
Harald Welteee3501f2021-04-02 13:00:18 +0200910 def _decode_bin(self, raw_bin_data:bytearray):
Harald Welteb2edd142021-01-08 23:29:35 +0100911 chunks = [raw_bin_data[i:i+self.rec_len] for i in range(0, len(raw_bin_data), self.rec_len)]
912 return [self.decode_record_bin(x) for x in chunks]
913
Harald Welteee3501f2021-04-02 13:00:18 +0200914 def _encode_bin(self, abstract_data) -> bytes:
Harald Welteb2edd142021-01-08 23:29:35 +0100915 chunks = [self.encode_record_bin(x) for x in abstract_data]
916 # FIXME: pad to file size
917 return b''.join(chunks)
918
919
Harald Welte27881622021-04-21 11:16:31 +0200920class BerTlvEF(TransparentEF):
921 """BER-TLV EF (Entry File) in the smart card filesystem.
922 A BER-TLV EF is a binary file with a BER (Basic Encoding Rules) TLV structure
Harald Welteb2edd142021-01-08 23:29:35 +0100923
Harald Welte27881622021-04-21 11:16:31 +0200924 NOTE: We currently don't really support those, this class is simply a wrapper
925 around TransparentEF as a place-holder, so we can already define EFs of BER-TLV
926 type without fully supporting them."""
Harald Welteb2edd142021-01-08 23:29:35 +0100927
928
929class RuntimeState(object):
930 """Represent the runtime state of a session with a card."""
Harald Welteee3501f2021-04-02 13:00:18 +0200931 def __init__(self, card, profile:'CardProfile'):
932 """
933 Args:
934 card : pysim.cards.Card instance
935 profile : CardProfile instance
936 """
Harald Welteb2edd142021-01-08 23:29:35 +0100937 self.mf = CardMF()
938 self.card = card
Harald Weltefe8a7442021-04-10 11:51:54 +0200939 self.selected_file = self.mf # type: CardDF
Harald Welteb2edd142021-01-08 23:29:35 +0100940 self.profile = profile
Harald Welte5ce35242021-04-02 20:27:05 +0200941 # add application ADFs + MF-files from profile
Philipp Maier1e896f32021-03-10 17:02:53 +0100942 apps = self._match_applications()
943 for a in apps:
Harald Welte5ce35242021-04-02 20:27:05 +0200944 if a.adf:
945 self.mf.add_application_df(a.adf)
Harald Welteb2edd142021-01-08 23:29:35 +0100946 for f in self.profile.files_in_mf:
947 self.mf.add_file(f)
Philipp Maier38c74f62021-03-17 17:19:52 +0100948 self.conserve_write = True
Harald Welteb2edd142021-01-08 23:29:35 +0100949
Philipp Maier1e896f32021-03-10 17:02:53 +0100950 def _match_applications(self):
951 """match the applications from the profile with applications on the card"""
952 apps_profile = self.profile.applications
953 aids_card = self.card.read_aids()
954 apps_taken = []
955 if aids_card:
956 aids_taken = []
957 print("AIDs on card:")
958 for a in aids_card:
959 for f in apps_profile:
960 if f.aid in a:
961 print(" %s: %s" % (f.name, a))
962 aids_taken.append(a)
963 apps_taken.append(f)
964 aids_unknown = set(aids_card) - set(aids_taken)
965 for a in aids_unknown:
966 print(" unknown: %s" % a)
967 else:
968 print("error: could not determine card applications")
969 return apps_taken
970
Harald Welteee3501f2021-04-02 13:00:18 +0200971 def get_cwd(self) -> CardDF:
972 """Obtain the current working directory.
973
974 Returns:
975 CardDF instance
976 """
Harald Welteb2edd142021-01-08 23:29:35 +0100977 if isinstance(self.selected_file, CardDF):
978 return self.selected_file
979 else:
980 return self.selected_file.parent
981
Harald Welte5ce35242021-04-02 20:27:05 +0200982 def get_application_df(self) -> Optional[CardADF]:
983 """Obtain the currently selected application DF (if any).
Harald Welteee3501f2021-04-02 13:00:18 +0200984
985 Returns:
986 CardADF() instance or None"""
Harald Welteb2edd142021-01-08 23:29:35 +0100987 # iterate upwards from selected file; check if any is an ADF
988 node = self.selected_file
989 while node.parent != node:
990 if isinstance(node, CardADF):
991 return node
992 node = node.parent
993 return None
994
Harald Welteee3501f2021-04-02 13:00:18 +0200995 def interpret_sw(self, sw:str):
996 """Interpret a given status word relative to the currently selected application
997 or the underlying card profile.
998
999 Args:
Harald Weltec9cdce32021-04-11 10:28:28 +02001000 sw : Status word as string of 4 hex digits
Harald Welteee3501f2021-04-02 13:00:18 +02001001
1002 Returns:
1003 Tuple of two strings
1004 """
Harald Welte86fbd392021-04-02 22:13:09 +02001005 res = None
Harald Welte5ce35242021-04-02 20:27:05 +02001006 adf = self.get_application_df()
1007 if adf:
1008 app = adf.application
Harald Welteb2edd142021-01-08 23:29:35 +01001009 # The application either comes with its own interpret_sw
1010 # method or we will use the interpret_sw method from the
1011 # card profile.
Harald Welte5ce35242021-04-02 20:27:05 +02001012 if app and hasattr(app, "interpret_sw"):
Harald Welte86fbd392021-04-02 22:13:09 +02001013 res = app.interpret_sw(sw)
1014 return res or self.profile.interpret_sw(sw)
Harald Welteb2edd142021-01-08 23:29:35 +01001015
Harald Welteee3501f2021-04-02 13:00:18 +02001016 def probe_file(self, fid:str, cmd_app=None):
1017 """Blindly try to select a file and automatically add a matching file
1018 object if the file actually exists."""
Philipp Maier63f572d2021-03-09 22:42:47 +01001019 if not is_hex(fid, 4, 4):
1020 raise ValueError("Cannot select unknown file by name %s, only hexadecimal 4 digit FID is allowed" % fid)
1021
1022 try:
1023 (data, sw) = self.card._scc.select_file(fid)
1024 except SwMatchError as swm:
1025 k = self.interpret_sw(swm.sw_actual)
1026 if not k:
1027 raise(swm)
1028 raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1]))
1029
1030 select_resp = self.selected_file.decode_select_response(data)
1031 if (select_resp['file_descriptor']['file_type'] == 'df'):
1032 f = CardDF(fid=fid, sfid=None, name="DF." + str(fid).upper(), desc="dedicated file, manually added at runtime")
1033 else:
1034 if (select_resp['file_descriptor']['structure'] == 'transparent'):
Harald Weltec9cdce32021-04-11 10:28:28 +02001035 f = TransparentEF(fid=fid, sfid=None, name="EF." + str(fid).upper(), desc="elementary file, manually added at runtime")
Philipp Maier63f572d2021-03-09 22:42:47 +01001036 else:
Harald Weltec9cdce32021-04-11 10:28:28 +02001037 f = LinFixedEF(fid=fid, sfid=None, name="EF." + str(fid).upper(), desc="elementary file, manually added at runtime")
Philipp Maier63f572d2021-03-09 22:42:47 +01001038
1039 self.selected_file.add_files([f])
1040 self.selected_file = f
1041 return select_resp
1042
Harald Welteee3501f2021-04-02 13:00:18 +02001043 def select(self, name:str, cmd_app=None):
1044 """Select a file (EF, DF, ADF, MF, ...).
1045
1046 Args:
1047 name : Name of file to select
1048 cmd_app : Command Application State (for unregistering old file commands)
1049 """
Harald Welteb2edd142021-01-08 23:29:35 +01001050 sels = self.selected_file.get_selectables()
Philipp Maier7744b6e2021-03-11 14:29:37 +01001051 if is_hex(name):
1052 name = name.lower()
Philipp Maier63f572d2021-03-09 22:42:47 +01001053
1054 # unregister commands of old file
1055 if cmd_app and self.selected_file.shell_commands:
1056 for c in self.selected_file.shell_commands:
1057 cmd_app.unregister_command_set(c)
1058
Harald Welteb2edd142021-01-08 23:29:35 +01001059 if name in sels:
1060 f = sels[name]
Harald Welteb2edd142021-01-08 23:29:35 +01001061 try:
1062 if isinstance(f, CardADF):
Philipp Maiercba6dbc2021-03-11 13:03:18 +01001063 (data, sw) = self.card.select_adf_by_aid(f.aid)
Harald Welteb2edd142021-01-08 23:29:35 +01001064 else:
1065 (data, sw) = self.card._scc.select_file(f.fid)
1066 self.selected_file = f
1067 except SwMatchError as swm:
1068 k = self.interpret_sw(swm.sw_actual)
1069 if not k:
1070 raise(swm)
1071 raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1]))
Philipp Maier63f572d2021-03-09 22:42:47 +01001072 select_resp = f.decode_select_response(data)
Harald Welteb2edd142021-01-08 23:29:35 +01001073 else:
Philipp Maier63f572d2021-03-09 22:42:47 +01001074 select_resp = self.probe_file(name, cmd_app)
Harald Welte850b72a2021-04-07 09:33:03 +02001075 # store the decoded FCP for later reference
1076 self.selected_file_fcp = select_resp
Philipp Maier63f572d2021-03-09 22:42:47 +01001077
1078 # register commands of new file
1079 if cmd_app and self.selected_file.shell_commands:
1080 for c in self.selected_file.shell_commands:
1081 cmd_app.register_command_set(c)
1082
1083 return select_resp
Harald Welteb2edd142021-01-08 23:29:35 +01001084
Harald Welteee3501f2021-04-02 13:00:18 +02001085 def read_binary(self, length:int=None, offset:int=0):
1086 """Read [part of] a transparent EF binary data.
1087
1088 Args:
1089 length : Amount of data to read (None: as much as possible)
1090 offset : Offset into the file from which to read 'length' bytes
1091 Returns:
1092 binary data read from the file
1093 """
Harald Welteb2edd142021-01-08 23:29:35 +01001094 if not isinstance(self.selected_file, TransparentEF):
1095 raise TypeError("Only works with TransparentEF")
1096 return self.card._scc.read_binary(self.selected_file.fid, length, offset)
1097
Harald Welte2d4a64b2021-04-03 09:01:24 +02001098 def read_binary_dec(self) -> Tuple[dict, str]:
Harald Welteee3501f2021-04-02 13:00:18 +02001099 """Read [part of] a transparent EF binary data and decode it.
1100
1101 Args:
1102 length : Amount of data to read (None: as much as possible)
1103 offset : Offset into the file from which to read 'length' bytes
1104 Returns:
1105 abstract decode data read from the file
1106 """
Harald Welteb2edd142021-01-08 23:29:35 +01001107 (data, sw) = self.read_binary()
1108 dec_data = self.selected_file.decode_hex(data)
Harald Welteb2edd142021-01-08 23:29:35 +01001109 return (dec_data, sw)
1110
Harald Welteee3501f2021-04-02 13:00:18 +02001111 def update_binary(self, data_hex:str, offset:int=0):
1112 """Update transparent EF binary data.
1113
1114 Args:
1115 data_hex : hex string of data to be written
1116 offset : Offset into the file from which to write 'data_hex'
1117 """
Harald Welteb2edd142021-01-08 23:29:35 +01001118 if not isinstance(self.selected_file, TransparentEF):
1119 raise TypeError("Only works with TransparentEF")
Philipp Maier38c74f62021-03-17 17:19:52 +01001120 return self.card._scc.update_binary(self.selected_file.fid, data_hex, offset, conserve=self.conserve_write)
Harald Welteb2edd142021-01-08 23:29:35 +01001121
Harald Welteee3501f2021-04-02 13:00:18 +02001122 def update_binary_dec(self, data:dict):
1123 """Update transparent EF from abstract data. Encodes the data to binary and
1124 then updates the EF with it.
1125
1126 Args:
1127 data : abstract data which is to be encoded and written
1128 """
Harald Welteb2edd142021-01-08 23:29:35 +01001129 data_hex = self.selected_file.encode_hex(data)
Harald Welteb2edd142021-01-08 23:29:35 +01001130 return self.update_binary(data_hex)
1131
Harald Welteee3501f2021-04-02 13:00:18 +02001132 def read_record(self, rec_nr:int=0):
1133 """Read a record as binary data.
1134
1135 Args:
1136 rec_nr : Record number to read
1137 Returns:
1138 hex string of binary data contained in record
1139 """
Harald Welteb2edd142021-01-08 23:29:35 +01001140 if not isinstance(self.selected_file, LinFixedEF):
1141 raise TypeError("Only works with Linear Fixed EF")
1142 # returns a string of hex nibbles
1143 return self.card._scc.read_record(self.selected_file.fid, rec_nr)
1144
Harald Welteee3501f2021-04-02 13:00:18 +02001145 def read_record_dec(self, rec_nr:int=0) -> Tuple[dict, str]:
1146 """Read a record and decode it to abstract data.
1147
1148 Args:
1149 rec_nr : Record number to read
1150 Returns:
1151 abstract data contained in record
1152 """
Harald Welteb2edd142021-01-08 23:29:35 +01001153 (data, sw) = self.read_record(rec_nr)
1154 return (self.selected_file.decode_record_hex(data), sw)
1155
Harald Welteee3501f2021-04-02 13:00:18 +02001156 def update_record(self, rec_nr:int, data_hex:str):
1157 """Update a record with given binary data
1158
1159 Args:
1160 rec_nr : Record number to read
1161 data_hex : Hex string binary data to be written
1162 """
Harald Welteb2edd142021-01-08 23:29:35 +01001163 if not isinstance(self.selected_file, LinFixedEF):
1164 raise TypeError("Only works with Linear Fixed EF")
Philipp Maier38c74f62021-03-17 17:19:52 +01001165 return self.card._scc.update_record(self.selected_file.fid, rec_nr, data_hex, conserve=self.conserve_write)
Harald Welteb2edd142021-01-08 23:29:35 +01001166
Harald Welteee3501f2021-04-02 13:00:18 +02001167 def update_record_dec(self, rec_nr:int, data:dict):
1168 """Update a record with given abstract data. Will encode abstract to binary data
1169 and then write it to the given record on the card.
1170
1171 Args:
1172 rec_nr : Record number to read
1173 data_hex : Abstract data to be written
1174 """
Harald Welte1e456572021-04-02 17:16:30 +02001175 data_hex = self.selected_file.encode_record_hex(data)
1176 return self.update_record(rec_nr, data_hex)
Harald Welteb2edd142021-01-08 23:29:35 +01001177
1178
1179
1180class FileData(object):
1181 """Represent the runtime, on-card data."""
1182 def __init__(self, fdesc):
1183 self.desc = fdesc
1184 self.fcp = None
1185
1186
Harald Welteee3501f2021-04-02 13:00:18 +02001187def interpret_sw(sw_data:dict, sw:str):
1188 """Interpret a given status word.
1189
1190 Args:
1191 sw_data : Hierarchical dict of status word matches
1192 sw : status word to match (string of 4 hex digits)
1193 Returns:
1194 tuple of two strings (class_string, description)
1195 """
Harald Welteb2edd142021-01-08 23:29:35 +01001196 for class_str, swdict in sw_data.items():
1197 # first try direct match
1198 if sw in swdict:
1199 return (class_str, swdict[sw])
1200 # next try wildcard matches
1201 for pattern, descr in swdict.items():
1202 if sw_match(sw, pattern):
1203 return (class_str, descr)
1204 return None
1205
1206class CardApplication(object):
1207 """A card application is represented by an ADF (with contained hierarchy) and optionally
1208 some SW definitions."""
Harald Welte5ce35242021-04-02 20:27:05 +02001209 def __init__(self, name, adf:Optional[CardADF]=None, aid:str=None, sw:dict=None):
Harald Welteee3501f2021-04-02 13:00:18 +02001210 """
1211 Args:
1212 adf : ADF name
1213 sw : Dict of status word conversions
1214 """
Harald Welteb2edd142021-01-08 23:29:35 +01001215 self.name = name
1216 self.adf = adf
Vadim Yanitskiy98f872b2021-03-27 01:25:46 +01001217 self.sw = sw or dict()
Harald Welte5ce35242021-04-02 20:27:05 +02001218 # back-reference from ADF to Applicaiton
1219 if self.adf:
1220 self.aid = aid or self.adf.aid
1221 self.adf.application = self
1222 else:
1223 self.aid = aid
Harald Welteb2edd142021-01-08 23:29:35 +01001224
1225 def __str__(self):
1226 return "APP(%s)" % (self.name)
1227
1228 def interpret_sw(self, sw):
Harald Welteee3501f2021-04-02 13:00:18 +02001229 """Interpret a given status word within the application.
1230
1231 Args:
Harald Weltec9cdce32021-04-11 10:28:28 +02001232 sw : Status word as string of 4 hex digits
Harald Welteee3501f2021-04-02 13:00:18 +02001233
1234 Returns:
1235 Tuple of two strings
1236 """
Harald Welteb2edd142021-01-08 23:29:35 +01001237 return interpret_sw(self.sw, sw)
1238
1239class CardProfile(object):
Harald Weltec9cdce32021-04-11 10:28:28 +02001240 """A Card Profile describes a card, it's filesystem hierarchy, an [initial] list of
Harald Welteb2edd142021-01-08 23:29:35 +01001241 applications as well as profile-specific SW and shell commands. Every card has
1242 one card profile, but there may be multiple applications within that profile."""
Vadim Yanitskiy98f872b2021-03-27 01:25:46 +01001243 def __init__(self, name, **kw):
Harald Welteee3501f2021-04-02 13:00:18 +02001244 """
1245 Args:
1246 desc (str) : Description
1247 files_in_mf : List of CardEF instances present in MF
1248 applications : List of CardApplications present on card
1249 sw : List of status word definitions
1250 shell_cmdsets : List of cmd2 shell command sets of profile-specific commands
1251 """
Harald Welteb2edd142021-01-08 23:29:35 +01001252 self.name = name
Vadim Yanitskiy98f872b2021-03-27 01:25:46 +01001253 self.desc = kw.get("desc", None)
1254 self.files_in_mf = kw.get("files_in_mf", [])
1255 self.sw = kw.get("sw", [])
1256 self.applications = kw.get("applications", [])
1257 self.shell_cmdsets = kw.get("shell_cmdsets", [])
Harald Welteb2edd142021-01-08 23:29:35 +01001258
1259 def __str__(self):
1260 return self.name
1261
Harald Welteee3501f2021-04-02 13:00:18 +02001262 def add_application(self, app:CardApplication):
1263 """Add an application to a card profile.
1264
1265 Args:
1266 app : CardApplication instance to be added to profile
1267 """
Philipp Maiereb72fa42021-03-26 21:29:57 +01001268 self.applications.append(app)
Harald Welteb2edd142021-01-08 23:29:35 +01001269
Harald Welteee3501f2021-04-02 13:00:18 +02001270 def interpret_sw(self, sw:str):
1271 """Interpret a given status word within the profile.
1272
1273 Args:
Harald Weltec9cdce32021-04-11 10:28:28 +02001274 sw : Status word as string of 4 hex digits
Harald Welteee3501f2021-04-02 13:00:18 +02001275
1276 Returns:
1277 Tuple of two strings
1278 """
Harald Welteb2edd142021-01-08 23:29:35 +01001279 return interpret_sw(self.sw, sw)