blob: 36588e25470609f12f724dae2bf952a01a1b0568 [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
212 raise ValueError("File with given fid %s already exists" % (child.fid))
213 if self.lookup_file_by_sfid(child.sfid):
214 raise ValueError("File with given sfid %s already exists" % (child.sfid))
215 if self.lookup_file_by_name(child.name):
216 if ignore_existing:
217 return
218 raise ValueError("File with given name %s already exists" % (child.name))
219 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')
641 upd_rec_dec_parser.add_argument('data', help='Data bytes (hex 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
691 rec_len : tuple of (minimum_length, recommended_length)
692 """
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
920
921
922
923class RuntimeState(object):
924 """Represent the runtime state of a session with a card."""
Harald Welteee3501f2021-04-02 13:00:18 +0200925 def __init__(self, card, profile:'CardProfile'):
926 """
927 Args:
928 card : pysim.cards.Card instance
929 profile : CardProfile instance
930 """
Harald Welteb2edd142021-01-08 23:29:35 +0100931 self.mf = CardMF()
932 self.card = card
Harald Weltefe8a7442021-04-10 11:51:54 +0200933 self.selected_file = self.mf # type: CardDF
Harald Welteb2edd142021-01-08 23:29:35 +0100934 self.profile = profile
Harald Welte5ce35242021-04-02 20:27:05 +0200935 # add application ADFs + MF-files from profile
Philipp Maier1e896f32021-03-10 17:02:53 +0100936 apps = self._match_applications()
937 for a in apps:
Harald Welte5ce35242021-04-02 20:27:05 +0200938 if a.adf:
939 self.mf.add_application_df(a.adf)
Harald Welteb2edd142021-01-08 23:29:35 +0100940 for f in self.profile.files_in_mf:
941 self.mf.add_file(f)
Philipp Maier38c74f62021-03-17 17:19:52 +0100942 self.conserve_write = True
Harald Welteb2edd142021-01-08 23:29:35 +0100943
Philipp Maier1e896f32021-03-10 17:02:53 +0100944 def _match_applications(self):
945 """match the applications from the profile with applications on the card"""
946 apps_profile = self.profile.applications
947 aids_card = self.card.read_aids()
948 apps_taken = []
949 if aids_card:
950 aids_taken = []
951 print("AIDs on card:")
952 for a in aids_card:
953 for f in apps_profile:
954 if f.aid in a:
955 print(" %s: %s" % (f.name, a))
956 aids_taken.append(a)
957 apps_taken.append(f)
958 aids_unknown = set(aids_card) - set(aids_taken)
959 for a in aids_unknown:
960 print(" unknown: %s" % a)
961 else:
962 print("error: could not determine card applications")
963 return apps_taken
964
Harald Welteee3501f2021-04-02 13:00:18 +0200965 def get_cwd(self) -> CardDF:
966 """Obtain the current working directory.
967
968 Returns:
969 CardDF instance
970 """
Harald Welteb2edd142021-01-08 23:29:35 +0100971 if isinstance(self.selected_file, CardDF):
972 return self.selected_file
973 else:
974 return self.selected_file.parent
975
Harald Welte5ce35242021-04-02 20:27:05 +0200976 def get_application_df(self) -> Optional[CardADF]:
977 """Obtain the currently selected application DF (if any).
Harald Welteee3501f2021-04-02 13:00:18 +0200978
979 Returns:
980 CardADF() instance or None"""
Harald Welteb2edd142021-01-08 23:29:35 +0100981 # iterate upwards from selected file; check if any is an ADF
982 node = self.selected_file
983 while node.parent != node:
984 if isinstance(node, CardADF):
985 return node
986 node = node.parent
987 return None
988
Harald Welteee3501f2021-04-02 13:00:18 +0200989 def interpret_sw(self, sw:str):
990 """Interpret a given status word relative to the currently selected application
991 or the underlying card profile.
992
993 Args:
Harald Weltec9cdce32021-04-11 10:28:28 +0200994 sw : Status word as string of 4 hex digits
Harald Welteee3501f2021-04-02 13:00:18 +0200995
996 Returns:
997 Tuple of two strings
998 """
Harald Welte86fbd392021-04-02 22:13:09 +0200999 res = None
Harald Welte5ce35242021-04-02 20:27:05 +02001000 adf = self.get_application_df()
1001 if adf:
1002 app = adf.application
Harald Welteb2edd142021-01-08 23:29:35 +01001003 # The application either comes with its own interpret_sw
1004 # method or we will use the interpret_sw method from the
1005 # card profile.
Harald Welte5ce35242021-04-02 20:27:05 +02001006 if app and hasattr(app, "interpret_sw"):
Harald Welte86fbd392021-04-02 22:13:09 +02001007 res = app.interpret_sw(sw)
1008 return res or self.profile.interpret_sw(sw)
Harald Welteb2edd142021-01-08 23:29:35 +01001009
Harald Welteee3501f2021-04-02 13:00:18 +02001010 def probe_file(self, fid:str, cmd_app=None):
1011 """Blindly try to select a file and automatically add a matching file
1012 object if the file actually exists."""
Philipp Maier63f572d2021-03-09 22:42:47 +01001013 if not is_hex(fid, 4, 4):
1014 raise ValueError("Cannot select unknown file by name %s, only hexadecimal 4 digit FID is allowed" % fid)
1015
1016 try:
1017 (data, sw) = self.card._scc.select_file(fid)
1018 except SwMatchError as swm:
1019 k = self.interpret_sw(swm.sw_actual)
1020 if not k:
1021 raise(swm)
1022 raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1]))
1023
1024 select_resp = self.selected_file.decode_select_response(data)
1025 if (select_resp['file_descriptor']['file_type'] == 'df'):
1026 f = CardDF(fid=fid, sfid=None, name="DF." + str(fid).upper(), desc="dedicated file, manually added at runtime")
1027 else:
1028 if (select_resp['file_descriptor']['structure'] == 'transparent'):
Harald Weltec9cdce32021-04-11 10:28:28 +02001029 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 +01001030 else:
Harald Weltec9cdce32021-04-11 10:28:28 +02001031 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 +01001032
1033 self.selected_file.add_files([f])
1034 self.selected_file = f
1035 return select_resp
1036
Harald Welteee3501f2021-04-02 13:00:18 +02001037 def select(self, name:str, cmd_app=None):
1038 """Select a file (EF, DF, ADF, MF, ...).
1039
1040 Args:
1041 name : Name of file to select
1042 cmd_app : Command Application State (for unregistering old file commands)
1043 """
Harald Welteb2edd142021-01-08 23:29:35 +01001044 sels = self.selected_file.get_selectables()
Philipp Maier7744b6e2021-03-11 14:29:37 +01001045 if is_hex(name):
1046 name = name.lower()
Philipp Maier63f572d2021-03-09 22:42:47 +01001047
1048 # unregister commands of old file
1049 if cmd_app and self.selected_file.shell_commands:
1050 for c in self.selected_file.shell_commands:
1051 cmd_app.unregister_command_set(c)
1052
Harald Welteb2edd142021-01-08 23:29:35 +01001053 if name in sels:
1054 f = sels[name]
Harald Welteb2edd142021-01-08 23:29:35 +01001055 try:
1056 if isinstance(f, CardADF):
Philipp Maiercba6dbc2021-03-11 13:03:18 +01001057 (data, sw) = self.card.select_adf_by_aid(f.aid)
Harald Welteb2edd142021-01-08 23:29:35 +01001058 else:
1059 (data, sw) = self.card._scc.select_file(f.fid)
1060 self.selected_file = f
1061 except SwMatchError as swm:
1062 k = self.interpret_sw(swm.sw_actual)
1063 if not k:
1064 raise(swm)
1065 raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1]))
Philipp Maier63f572d2021-03-09 22:42:47 +01001066 select_resp = f.decode_select_response(data)
Harald Welteb2edd142021-01-08 23:29:35 +01001067 else:
Philipp Maier63f572d2021-03-09 22:42:47 +01001068 select_resp = self.probe_file(name, cmd_app)
Harald Welte850b72a2021-04-07 09:33:03 +02001069 # store the decoded FCP for later reference
1070 self.selected_file_fcp = select_resp
Philipp Maier63f572d2021-03-09 22:42:47 +01001071
1072 # register commands of new file
1073 if cmd_app and self.selected_file.shell_commands:
1074 for c in self.selected_file.shell_commands:
1075 cmd_app.register_command_set(c)
1076
1077 return select_resp
Harald Welteb2edd142021-01-08 23:29:35 +01001078
Harald Welteee3501f2021-04-02 13:00:18 +02001079 def read_binary(self, length:int=None, offset:int=0):
1080 """Read [part of] a transparent EF binary data.
1081
1082 Args:
1083 length : Amount of data to read (None: as much as possible)
1084 offset : Offset into the file from which to read 'length' bytes
1085 Returns:
1086 binary data read from the file
1087 """
Harald Welteb2edd142021-01-08 23:29:35 +01001088 if not isinstance(self.selected_file, TransparentEF):
1089 raise TypeError("Only works with TransparentEF")
1090 return self.card._scc.read_binary(self.selected_file.fid, length, offset)
1091
Harald Welte2d4a64b2021-04-03 09:01:24 +02001092 def read_binary_dec(self) -> Tuple[dict, str]:
Harald Welteee3501f2021-04-02 13:00:18 +02001093 """Read [part of] a transparent EF binary data and decode it.
1094
1095 Args:
1096 length : Amount of data to read (None: as much as possible)
1097 offset : Offset into the file from which to read 'length' bytes
1098 Returns:
1099 abstract decode data read from the file
1100 """
Harald Welteb2edd142021-01-08 23:29:35 +01001101 (data, sw) = self.read_binary()
1102 dec_data = self.selected_file.decode_hex(data)
Harald Welteb2edd142021-01-08 23:29:35 +01001103 return (dec_data, sw)
1104
Harald Welteee3501f2021-04-02 13:00:18 +02001105 def update_binary(self, data_hex:str, offset:int=0):
1106 """Update transparent EF binary data.
1107
1108 Args:
1109 data_hex : hex string of data to be written
1110 offset : Offset into the file from which to write 'data_hex'
1111 """
Harald Welteb2edd142021-01-08 23:29:35 +01001112 if not isinstance(self.selected_file, TransparentEF):
1113 raise TypeError("Only works with TransparentEF")
Philipp Maier38c74f62021-03-17 17:19:52 +01001114 return self.card._scc.update_binary(self.selected_file.fid, data_hex, offset, conserve=self.conserve_write)
Harald Welteb2edd142021-01-08 23:29:35 +01001115
Harald Welteee3501f2021-04-02 13:00:18 +02001116 def update_binary_dec(self, data:dict):
1117 """Update transparent EF from abstract data. Encodes the data to binary and
1118 then updates the EF with it.
1119
1120 Args:
1121 data : abstract data which is to be encoded and written
1122 """
Harald Welteb2edd142021-01-08 23:29:35 +01001123 data_hex = self.selected_file.encode_hex(data)
Harald Welteb2edd142021-01-08 23:29:35 +01001124 return self.update_binary(data_hex)
1125
Harald Welteee3501f2021-04-02 13:00:18 +02001126 def read_record(self, rec_nr:int=0):
1127 """Read a record as binary data.
1128
1129 Args:
1130 rec_nr : Record number to read
1131 Returns:
1132 hex string of binary data contained in record
1133 """
Harald Welteb2edd142021-01-08 23:29:35 +01001134 if not isinstance(self.selected_file, LinFixedEF):
1135 raise TypeError("Only works with Linear Fixed EF")
1136 # returns a string of hex nibbles
1137 return self.card._scc.read_record(self.selected_file.fid, rec_nr)
1138
Harald Welteee3501f2021-04-02 13:00:18 +02001139 def read_record_dec(self, rec_nr:int=0) -> Tuple[dict, str]:
1140 """Read a record and decode it to abstract data.
1141
1142 Args:
1143 rec_nr : Record number to read
1144 Returns:
1145 abstract data contained in record
1146 """
Harald Welteb2edd142021-01-08 23:29:35 +01001147 (data, sw) = self.read_record(rec_nr)
1148 return (self.selected_file.decode_record_hex(data), sw)
1149
Harald Welteee3501f2021-04-02 13:00:18 +02001150 def update_record(self, rec_nr:int, data_hex:str):
1151 """Update a record with given binary data
1152
1153 Args:
1154 rec_nr : Record number to read
1155 data_hex : Hex string binary data to be written
1156 """
Harald Welteb2edd142021-01-08 23:29:35 +01001157 if not isinstance(self.selected_file, LinFixedEF):
1158 raise TypeError("Only works with Linear Fixed EF")
Philipp Maier38c74f62021-03-17 17:19:52 +01001159 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 +01001160
Harald Welteee3501f2021-04-02 13:00:18 +02001161 def update_record_dec(self, rec_nr:int, data:dict):
1162 """Update a record with given abstract data. Will encode abstract to binary data
1163 and then write it to the given record on the card.
1164
1165 Args:
1166 rec_nr : Record number to read
1167 data_hex : Abstract data to be written
1168 """
Harald Welte1e456572021-04-02 17:16:30 +02001169 data_hex = self.selected_file.encode_record_hex(data)
1170 return self.update_record(rec_nr, data_hex)
Harald Welteb2edd142021-01-08 23:29:35 +01001171
1172
1173
1174class FileData(object):
1175 """Represent the runtime, on-card data."""
1176 def __init__(self, fdesc):
1177 self.desc = fdesc
1178 self.fcp = None
1179
1180
Harald Welteee3501f2021-04-02 13:00:18 +02001181def interpret_sw(sw_data:dict, sw:str):
1182 """Interpret a given status word.
1183
1184 Args:
1185 sw_data : Hierarchical dict of status word matches
1186 sw : status word to match (string of 4 hex digits)
1187 Returns:
1188 tuple of two strings (class_string, description)
1189 """
Harald Welteb2edd142021-01-08 23:29:35 +01001190 for class_str, swdict in sw_data.items():
1191 # first try direct match
1192 if sw in swdict:
1193 return (class_str, swdict[sw])
1194 # next try wildcard matches
1195 for pattern, descr in swdict.items():
1196 if sw_match(sw, pattern):
1197 return (class_str, descr)
1198 return None
1199
1200class CardApplication(object):
1201 """A card application is represented by an ADF (with contained hierarchy) and optionally
1202 some SW definitions."""
Harald Welte5ce35242021-04-02 20:27:05 +02001203 def __init__(self, name, adf:Optional[CardADF]=None, aid:str=None, sw:dict=None):
Harald Welteee3501f2021-04-02 13:00:18 +02001204 """
1205 Args:
1206 adf : ADF name
1207 sw : Dict of status word conversions
1208 """
Harald Welteb2edd142021-01-08 23:29:35 +01001209 self.name = name
1210 self.adf = adf
Vadim Yanitskiy98f872b2021-03-27 01:25:46 +01001211 self.sw = sw or dict()
Harald Welte5ce35242021-04-02 20:27:05 +02001212 # back-reference from ADF to Applicaiton
1213 if self.adf:
1214 self.aid = aid or self.adf.aid
1215 self.adf.application = self
1216 else:
1217 self.aid = aid
Harald Welteb2edd142021-01-08 23:29:35 +01001218
1219 def __str__(self):
1220 return "APP(%s)" % (self.name)
1221
1222 def interpret_sw(self, sw):
Harald Welteee3501f2021-04-02 13:00:18 +02001223 """Interpret a given status word within the application.
1224
1225 Args:
Harald Weltec9cdce32021-04-11 10:28:28 +02001226 sw : Status word as string of 4 hex digits
Harald Welteee3501f2021-04-02 13:00:18 +02001227
1228 Returns:
1229 Tuple of two strings
1230 """
Harald Welteb2edd142021-01-08 23:29:35 +01001231 return interpret_sw(self.sw, sw)
1232
1233class CardProfile(object):
Harald Weltec9cdce32021-04-11 10:28:28 +02001234 """A Card Profile describes a card, it's filesystem hierarchy, an [initial] list of
Harald Welteb2edd142021-01-08 23:29:35 +01001235 applications as well as profile-specific SW and shell commands. Every card has
1236 one card profile, but there may be multiple applications within that profile."""
Vadim Yanitskiy98f872b2021-03-27 01:25:46 +01001237 def __init__(self, name, **kw):
Harald Welteee3501f2021-04-02 13:00:18 +02001238 """
1239 Args:
1240 desc (str) : Description
1241 files_in_mf : List of CardEF instances present in MF
1242 applications : List of CardApplications present on card
1243 sw : List of status word definitions
1244 shell_cmdsets : List of cmd2 shell command sets of profile-specific commands
1245 """
Harald Welteb2edd142021-01-08 23:29:35 +01001246 self.name = name
Vadim Yanitskiy98f872b2021-03-27 01:25:46 +01001247 self.desc = kw.get("desc", None)
1248 self.files_in_mf = kw.get("files_in_mf", [])
1249 self.sw = kw.get("sw", [])
1250 self.applications = kw.get("applications", [])
1251 self.shell_cmdsets = kw.get("shell_cmdsets", [])
Harald Welteb2edd142021-01-08 23:29:35 +01001252
1253 def __str__(self):
1254 return self.name
1255
Harald Welteee3501f2021-04-02 13:00:18 +02001256 def add_application(self, app:CardApplication):
1257 """Add an application to a card profile.
1258
1259 Args:
1260 app : CardApplication instance to be added to profile
1261 """
Philipp Maiereb72fa42021-03-26 21:29:57 +01001262 self.applications.append(app)
Harald Welteb2edd142021-01-08 23:29:35 +01001263
Harald Welteee3501f2021-04-02 13:00:18 +02001264 def interpret_sw(self, sw:str):
1265 """Interpret a given status word within the profile.
1266
1267 Args:
Harald Weltec9cdce32021-04-11 10:28:28 +02001268 sw : Status word as string of 4 hex digits
Harald Welteee3501f2021-04-02 13:00:18 +02001269
1270 Returns:
1271 Tuple of two strings
1272 """
Harald Welteb2edd142021-01-08 23:29:35 +01001273 return interpret_sw(self.sw, sw)