blob: e771f3cd13b8d03afbe34f2bd38b906742635e1b [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 Welteb2edd142021-01-08 23:29:35 +010038from pySim.exceptions import *
Harald Welte0d4e98a2021-04-07 00:14:40 +020039from pySim.jsonpath import js_path_find, js_path_modify
Harald Welteb2edd142021-01-08 23:29:35 +010040
41class CardFile(object):
42 """Base class for all objects in the smart card filesystem.
43 Serve as a common ancestor to all other file types; rarely used directly.
44 """
45 RESERVED_NAMES = ['..', '.', '/', 'MF']
46 RESERVED_FIDS = ['3f00']
47
Harald Welteee3501f2021-04-02 13:00:18 +020048 def __init__(self, fid:str=None, sfid:str=None, name:str=None, desc:str=None,
49 parent:Optional['CardDF']=None):
50 """
51 Args:
52 fid : File Identifier (4 hex digits)
53 sfid : Short File Identifier (2 hex digits, optional)
54 name : Brief name of the file, lik EF_ICCID
55 desc : Descriptoin of the file
56 parent : Parent CardFile object within filesystem hierarchy
57 """
Harald Welteb2edd142021-01-08 23:29:35 +010058 if not isinstance(self, CardADF) and fid == None:
59 raise ValueError("fid is mandatory")
60 if fid:
61 fid = fid.lower()
62 self.fid = fid # file identifier
63 self.sfid = sfid # short file identifier
64 self.name = name # human readable name
65 self.desc = desc # human readable description
66 self.parent = parent
67 if self.parent and self.parent != self and self.fid:
68 self.parent.add_file(self)
Vadim Yanitskiya0040792021-04-09 22:44:44 +020069 self.shell_commands = [] # type: List[CommandSet]
Harald Welteb2edd142021-01-08 23:29:35 +010070
Philipp Maier66061582021-03-09 21:57:57 +010071 # Note: the basic properties (fid, name, ect.) are verified when
72 # the file is attached to a parent file. See method add_file() in
73 # class Card DF
74
Harald Welteb2edd142021-01-08 23:29:35 +010075 def __str__(self):
76 if self.name:
77 return self.name
78 else:
79 return self.fid
80
Harald Welteee3501f2021-04-02 13:00:18 +020081 def _path_element(self, prefer_name:bool) -> Optional[str]:
Harald Welteb2edd142021-01-08 23:29:35 +010082 if prefer_name and self.name:
83 return self.name
84 else:
85 return self.fid
86
Harald Welte1e456572021-04-02 17:16:30 +020087 def fully_qualified_path(self, prefer_name:bool=True) -> List[str]:
Harald Welteee3501f2021-04-02 13:00:18 +020088 """Return fully qualified path to file as list of FID or name strings.
89
90 Args:
91 prefer_name : Preferably build path of names; fall-back to FIDs as required
92 """
Harald Welte1e456572021-04-02 17:16:30 +020093 if self.parent and self.parent != self:
Harald Welteb2edd142021-01-08 23:29:35 +010094 ret = self.parent.fully_qualified_path(prefer_name)
95 else:
96 ret = []
Harald Welte1e456572021-04-02 17:16:30 +020097 elem = self._path_element(prefer_name)
98 if elem:
99 ret.append(elem)
Harald Welteb2edd142021-01-08 23:29:35 +0100100 return ret
101
Harald Welteee3501f2021-04-02 13:00:18 +0200102 def get_mf(self) -> Optional['CardMF']:
Harald Welteb2edd142021-01-08 23:29:35 +0100103 """Return the MF (root) of the file system."""
104 if self.parent == None:
105 return None
106 # iterate towards the top. MF has parent == self
107 node = self
Harald Welte1e456572021-04-02 17:16:30 +0200108 while node.parent and node.parent != node:
Harald Welteb2edd142021-01-08 23:29:35 +0100109 node = node.parent
Harald Welte1e456572021-04-02 17:16:30 +0200110 return cast(CardMF, node)
Harald Welteb2edd142021-01-08 23:29:35 +0100111
Harald Welte1e456572021-04-02 17:16:30 +0200112 def _get_self_selectables(self, alias:str=None, flags = []) -> Dict[str, 'CardFile']:
Harald Welteee3501f2021-04-02 13:00:18 +0200113 """Return a dict of {'identifier': self} tuples.
114
115 Args:
116 alias : Add an alias with given name to 'self'
117 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
118 If not specified, all selectables will be returned.
119 Returns:
120 dict containing reference to 'self' for all identifiers.
121 """
Harald Welteb2edd142021-01-08 23:29:35 +0100122 sels = {}
123 if alias:
124 sels.update({alias: self})
Philipp Maier786f7812021-02-25 16:48:10 +0100125 if self.fid and (flags == [] or 'FIDS' in flags):
Harald Welteb2edd142021-01-08 23:29:35 +0100126 sels.update({self.fid: self})
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100127 if self.name and (flags == [] or 'FNAMES' in flags):
Harald Welteb2edd142021-01-08 23:29:35 +0100128 sels.update({self.name: self})
129 return sels
130
Harald Welte1e456572021-04-02 17:16:30 +0200131 def get_selectables(self, flags = []) -> Dict[str, 'CardFile']:
Harald Welteee3501f2021-04-02 13:00:18 +0200132 """Return a dict of {'identifier': File} that is selectable from the current file.
133
134 Args:
135 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
136 If not specified, all selectables will be returned.
137 Returns:
138 dict containing all selectable items. Key is identifier (string), value
139 a reference to a CardFile (or derived class) instance.
140 """
Philipp Maier786f7812021-02-25 16:48:10 +0100141 sels = {}
Harald Welteb2edd142021-01-08 23:29:35 +0100142 # we can always select ourself
Philipp Maier786f7812021-02-25 16:48:10 +0100143 if flags == [] or 'SELF' in flags:
144 sels = self._get_self_selectables('.', flags)
Harald Welteb2edd142021-01-08 23:29:35 +0100145 # we can always select our parent
Philipp Maier786f7812021-02-25 16:48:10 +0100146 if flags == [] or 'PARENT' in flags:
Harald Welte1e456572021-04-02 17:16:30 +0200147 if self.parent:
148 sels = self.parent._get_self_selectables('..', flags)
Harald Welteb2edd142021-01-08 23:29:35 +0100149 # if we have a MF, we can always select its applications
Philipp Maier786f7812021-02-25 16:48:10 +0100150 if flags == [] or 'MF' in flags:
151 mf = self.get_mf()
152 if mf:
153 sels.update(mf._get_self_selectables(flags = flags))
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100154 sels.update(mf.get_app_selectables(flags = flags))
Harald Welteb2edd142021-01-08 23:29:35 +0100155 return sels
156
Harald Welte1e456572021-04-02 17:16:30 +0200157 def get_selectable_names(self, flags = []) -> List[str]:
Harald Welteee3501f2021-04-02 13:00:18 +0200158 """Return a dict of {'identifier': File} that is selectable from the current file.
159
160 Args:
161 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
162 If not specified, all selectables will be returned.
163 Returns:
Harald Welte1e456572021-04-02 17:16:30 +0200164 list containing all selectable names.
Harald Welteee3501f2021-04-02 13:00:18 +0200165 """
Philipp Maier786f7812021-02-25 16:48:10 +0100166 sels = self.get_selectables(flags)
Harald Welte1e456572021-04-02 17:16:30 +0200167 return list(sels.keys())
Harald Welteb2edd142021-01-08 23:29:35 +0100168
Harald Welteee3501f2021-04-02 13:00:18 +0200169 def decode_select_response(self, data_hex:str):
Harald Welteb2edd142021-01-08 23:29:35 +0100170 """Decode the response to a SELECT command."""
Harald Welte1e456572021-04-02 17:16:30 +0200171 if self.parent:
172 return self.parent.decode_select_response(data_hex)
Harald Welteb2edd142021-01-08 23:29:35 +0100173
174
175class CardDF(CardFile):
176 """DF (Dedicated File) in the smart card filesystem. Those are basically sub-directories."""
Philipp Maier63f572d2021-03-09 22:42:47 +0100177
178 @with_default_category('DF/ADF Commands')
179 class ShellCommands(CommandSet):
180 def __init__(self):
181 super().__init__()
182
Harald Welteb2edd142021-01-08 23:29:35 +0100183 def __init__(self, **kwargs):
184 if not isinstance(self, CardADF):
185 if not 'fid' in kwargs:
186 raise TypeError('fid is mandatory for all DF')
187 super().__init__(**kwargs)
188 self.children = dict()
Philipp Maier63f572d2021-03-09 22:42:47 +0100189 self.shell_commands = [self.ShellCommands()]
Harald Welteb2edd142021-01-08 23:29:35 +0100190
191 def __str__(self):
192 return "DF(%s)" % (super().__str__())
193
Harald Welteee3501f2021-04-02 13:00:18 +0200194 def add_file(self, child:CardFile, ignore_existing:bool=False):
195 """Add a child (DF/EF) to this DF.
196 Args:
197 child: The new DF/EF to be added
198 ignore_existing: Ignore, if file with given FID already exists. Old one will be kept.
199 """
Harald Welteb2edd142021-01-08 23:29:35 +0100200 if not isinstance(child, CardFile):
201 raise TypeError("Expected a File instance")
Philipp Maier3aec8712021-03-09 21:49:01 +0100202 if not is_hex(child.fid, minlen = 4, maxlen = 4):
203 raise ValueError("File name %s is not a valid fid" % (child.fid))
Harald Welteb2edd142021-01-08 23:29:35 +0100204 if child.name in CardFile.RESERVED_NAMES:
205 raise ValueError("File name %s is a reserved name" % (child.name))
206 if child.fid in CardFile.RESERVED_FIDS:
Philipp Maiere8bc1b42021-03-09 20:33:41 +0100207 raise ValueError("File fid %s is a reserved fid" % (child.fid))
Harald Welteb2edd142021-01-08 23:29:35 +0100208 if child.fid in self.children:
209 if ignore_existing:
210 return
211 raise ValueError("File with given fid %s already exists" % (child.fid))
212 if self.lookup_file_by_sfid(child.sfid):
213 raise ValueError("File with given sfid %s already exists" % (child.sfid))
214 if self.lookup_file_by_name(child.name):
215 if ignore_existing:
216 return
217 raise ValueError("File with given name %s already exists" % (child.name))
218 self.children[child.fid] = child
219 child.parent = self
220
Harald Welteee3501f2021-04-02 13:00:18 +0200221 def add_files(self, children:Iterable[CardFile], ignore_existing:bool=False):
222 """Add a list of child (DF/EF) to this DF
223
224 Args:
225 children: List of new DF/EFs to be added
226 ignore_existing: Ignore, if file[s] with given FID already exists. Old one[s] will be kept.
227 """
Harald Welteb2edd142021-01-08 23:29:35 +0100228 for child in children:
229 self.add_file(child, ignore_existing)
230
Harald Welteee3501f2021-04-02 13:00:18 +0200231 def get_selectables(self, flags = []) -> dict:
232 """Return a dict of {'identifier': File} that is selectable from the current DF.
233
234 Args:
235 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
236 If not specified, all selectables will be returned.
237 Returns:
238 dict containing all selectable items. Key is identifier (string), value
239 a reference to a CardFile (or derived class) instance.
240 """
Harald Welteb2edd142021-01-08 23:29:35 +0100241 # global selectables + our children
Philipp Maier786f7812021-02-25 16:48:10 +0100242 sels = super().get_selectables(flags)
243 if flags == [] or 'FIDS' in flags:
244 sels.update({x.fid: x for x in self.children.values() if x.fid})
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100245 if flags == [] or 'FNAMES' in flags:
Philipp Maier786f7812021-02-25 16:48:10 +0100246 sels.update({x.name: x for x in self.children.values() if x.name})
Harald Welteb2edd142021-01-08 23:29:35 +0100247 return sels
248
Harald Welte1e456572021-04-02 17:16:30 +0200249 def lookup_file_by_name(self, name:Optional[str]) -> Optional[CardFile]:
Harald Welteee3501f2021-04-02 13:00:18 +0200250 """Find a file with given name within current DF."""
Harald Welteb2edd142021-01-08 23:29:35 +0100251 if name == None:
252 return None
253 for i in self.children.values():
254 if i.name and i.name == name:
255 return i
256 return None
257
Harald Welte1e456572021-04-02 17:16:30 +0200258 def lookup_file_by_sfid(self, sfid:Optional[str]) -> Optional[CardFile]:
Harald Welteee3501f2021-04-02 13:00:18 +0200259 """Find a file with given short file ID within current DF."""
Harald Welteb2edd142021-01-08 23:29:35 +0100260 if sfid == None:
261 return None
262 for i in self.children.values():
Harald Welte1e456572021-04-02 17:16:30 +0200263 if i.sfid == int(str(sfid)):
Harald Welteb2edd142021-01-08 23:29:35 +0100264 return i
265 return None
266
Harald Welteee3501f2021-04-02 13:00:18 +0200267 def lookup_file_by_fid(self, fid:str) -> Optional[CardFile]:
268 """Find a file with given file ID within current DF."""
Harald Welteb2edd142021-01-08 23:29:35 +0100269 if fid in self.children:
270 return self.children[fid]
271 return None
272
273
274class CardMF(CardDF):
275 """MF (Master File) in the smart card filesystem"""
276 def __init__(self, **kwargs):
277 # can be overridden; use setdefault
278 kwargs.setdefault('fid', '3f00')
279 kwargs.setdefault('name', 'MF')
280 kwargs.setdefault('desc', 'Master File (directory root)')
281 # cannot be overridden; use assignment
282 kwargs['parent'] = self
283 super().__init__(**kwargs)
284 self.applications = dict()
285
286 def __str__(self):
287 return "MF(%s)" % (self.fid)
288
Harald Welte5ce35242021-04-02 20:27:05 +0200289 def add_application_df(self, app:'CardADF'):
290 """Add an Application to the MF"""
Harald Welteb2edd142021-01-08 23:29:35 +0100291 if not isinstance(app, CardADF):
292 raise TypeError("Expected an ADF instance")
293 if app.aid in self.applications:
294 raise ValueError("AID %s already exists" % (app.aid))
295 self.applications[app.aid] = app
296 app.parent=self
297
298 def get_app_names(self):
299 """Get list of completions (AID names)"""
300 return [x.name for x in self.applications]
301
Harald Welteee3501f2021-04-02 13:00:18 +0200302 def get_selectables(self, flags = []) -> dict:
303 """Return a dict of {'identifier': File} that is selectable from the current DF.
304
305 Args:
306 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
307 If not specified, all selectables will be returned.
308 Returns:
309 dict containing all selectable items. Key is identifier (string), value
310 a reference to a CardFile (or derived class) instance.
311 """
Philipp Maier786f7812021-02-25 16:48:10 +0100312 sels = super().get_selectables(flags)
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100313 sels.update(self.get_app_selectables(flags))
Harald Welteb2edd142021-01-08 23:29:35 +0100314 return sels
315
Harald Welteee3501f2021-04-02 13:00:18 +0200316 def get_app_selectables(self, flags = []) -> dict:
Philipp Maier786f7812021-02-25 16:48:10 +0100317 """Get applications by AID + name"""
318 sels = {}
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100319 if flags == [] or 'AIDS' in flags:
Philipp Maier786f7812021-02-25 16:48:10 +0100320 sels.update({x.aid: x for x in self.applications.values()})
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100321 if flags == [] or 'ANAMES' in flags:
Philipp Maier786f7812021-02-25 16:48:10 +0100322 sels.update({x.name: x for x in self.applications.values() if x.name})
Harald Welteb2edd142021-01-08 23:29:35 +0100323 return sels
324
Harald Welteee3501f2021-04-02 13:00:18 +0200325 def decode_select_response(self, data_hex:str) -> Any:
326 """Decode the response to a SELECT command.
327
328 This is the fall-back method which doesn't perform any decoding. It mostly
329 exists so specific derived classes can overload it for actual decoding.
330 """
Harald Welteb2edd142021-01-08 23:29:35 +0100331 return data_hex
332
333
334
335class CardADF(CardDF):
336 """ADF (Application Dedicated File) in the smart card filesystem"""
Harald Welteee3501f2021-04-02 13:00:18 +0200337 def __init__(self, aid:str, **kwargs):
Harald Welteb2edd142021-01-08 23:29:35 +0100338 super().__init__(**kwargs)
Harald Welte5ce35242021-04-02 20:27:05 +0200339 # reference to CardApplication may be set from CardApplication constructor
Harald Weltefe8a7442021-04-10 11:51:54 +0200340 self.application = None # type: Optional[CardApplication]
Harald Welteb2edd142021-01-08 23:29:35 +0100341 self.aid = aid # Application Identifier
Harald Welte1e456572021-04-02 17:16:30 +0200342 mf = self.get_mf()
343 if mf:
Harald Welte5ce35242021-04-02 20:27:05 +0200344 mf.add_application_df(self)
Harald Welteb2edd142021-01-08 23:29:35 +0100345
346 def __str__(self):
347 return "ADF(%s)" % (self.aid)
348
Harald Welteee3501f2021-04-02 13:00:18 +0200349 def _path_element(self, prefer_name:bool):
Harald Welteb2edd142021-01-08 23:29:35 +0100350 if self.name and prefer_name:
351 return self.name
352 else:
353 return self.aid
354
355
356class CardEF(CardFile):
357 """EF (Entry File) in the smart card filesystem"""
358 def __init__(self, *, fid, **kwargs):
359 kwargs['fid'] = fid
360 super().__init__(**kwargs)
361
362 def __str__(self):
363 return "EF(%s)" % (super().__str__())
364
Harald Welteee3501f2021-04-02 13:00:18 +0200365 def get_selectables(self, flags = []) -> dict:
366 """Return a dict of {'identifier': File} that is selectable from the current DF.
367
368 Args:
369 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
370 If not specified, all selectables will be returned.
371 Returns:
372 dict containing all selectable items. Key is identifier (string), value
373 a reference to a CardFile (or derived class) instance.
374 """
Harald Welteb2edd142021-01-08 23:29:35 +0100375 #global selectable names + those of the parent DF
Philipp Maier786f7812021-02-25 16:48:10 +0100376 sels = super().get_selectables(flags)
Harald Welteb2edd142021-01-08 23:29:35 +0100377 sels.update({x.name:x for x in self.parent.children.values() if x != self})
378 return sels
379
380
381class TransparentEF(CardEF):
Harald Welteee3501f2021-04-02 13:00:18 +0200382 """Transparent EF (Entry File) in the smart card filesystem.
383
384 A Transparent EF is a binary file with no formal structure. This is contrary to
385 Record based EFs which have [fixed size] records that can be individually read/updated."""
Harald Welteb2edd142021-01-08 23:29:35 +0100386
387 @with_default_category('Transparent EF Commands')
388 class ShellCommands(CommandSet):
Harald Welteee3501f2021-04-02 13:00:18 +0200389 """Shell commands specific for Trransparent EFs."""
Harald Welteb2edd142021-01-08 23:29:35 +0100390 def __init__(self):
391 super().__init__()
392
393 read_bin_parser = argparse.ArgumentParser()
394 read_bin_parser.add_argument('--offset', type=int, default=0, help='Byte offset for start of read')
395 read_bin_parser.add_argument('--length', type=int, help='Number of bytes to read')
396 @cmd2.with_argparser(read_bin_parser)
397 def do_read_binary(self, opts):
398 """Read binary data from a transparent EF"""
399 (data, sw) = self._cmd.rs.read_binary(opts.length, opts.offset)
400 self._cmd.poutput(data)
401
Harald Weltebcad86c2021-04-06 20:08:39 +0200402 read_bin_dec_parser = argparse.ArgumentParser()
403 read_bin_dec_parser.add_argument('--oneline', action='store_true',
404 help='No JSON pretty-printing, dump as a single line')
405 @cmd2.with_argparser(read_bin_dec_parser)
Harald Welteb2edd142021-01-08 23:29:35 +0100406 def do_read_binary_decoded(self, opts):
407 """Read + decode data from a transparent EF"""
408 (data, sw) = self._cmd.rs.read_binary_dec()
Harald Welte1748b932021-04-06 21:12:25 +0200409 self._cmd.poutput_json(data, opts.oneline)
Harald Welteb2edd142021-01-08 23:29:35 +0100410
411 upd_bin_parser = argparse.ArgumentParser()
412 upd_bin_parser.add_argument('--offset', type=int, default=0, help='Byte offset for start of read')
413 upd_bin_parser.add_argument('data', help='Data bytes (hex format) to write')
414 @cmd2.with_argparser(upd_bin_parser)
415 def do_update_binary(self, opts):
416 """Update (Write) data of a transparent EF"""
417 (data, sw) = self._cmd.rs.update_binary(opts.data, opts.offset)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100418 if data:
419 self._cmd.poutput(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100420
421 upd_bin_dec_parser = argparse.ArgumentParser()
422 upd_bin_dec_parser.add_argument('data', help='Abstract data (JSON format) to write')
Harald Welte0d4e98a2021-04-07 00:14:40 +0200423 upd_bin_dec_parser.add_argument('--json-path', type=str,
424 help='JSON path to modify specific element of file only')
Harald Welteb2edd142021-01-08 23:29:35 +0100425 @cmd2.with_argparser(upd_bin_dec_parser)
426 def do_update_binary_decoded(self, opts):
427 """Encode + Update (Write) data of a transparent EF"""
Harald Welte0d4e98a2021-04-07 00:14:40 +0200428 if opts.json_path:
429 (data_json, sw) = self._cmd.rs.read_binary_dec()
430 js_path_modify(data_json, opts.json_path, json.loads(opts.data))
431 else:
432 data_json = json.loads(opts.data)
Harald Welteb2edd142021-01-08 23:29:35 +0100433 (data, sw) = self._cmd.rs.update_binary_dec(data_json)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100434 if data:
Harald Welte1748b932021-04-06 21:12:25 +0200435 self._cmd.poutput_json(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100436
Harald Welte4145d3c2021-04-08 20:34:13 +0200437 def do_edit_binary_decoded(self, opts):
438 """Edit the JSON representation of the EF contents in an editor."""
439 (orig_json, sw) = self._cmd.rs.read_binary_dec()
440 with tempfile.TemporaryDirectory(prefix='pysim_') as dirname:
441 filename = '%s/file' % dirname
442 # write existing data as JSON to file
443 with open(filename, 'w') as text_file:
444 json.dump(orig_json, text_file, indent=4)
445 # run a text editor
446 self._cmd._run_editor(filename)
447 with open(filename, 'r') as text_file:
448 edited_json = json.load(text_file)
449 if edited_json == orig_json:
450 self._cmd.poutput("Data not modified, skipping write")
451 else:
452 (data, sw) = self._cmd.rs.update_binary_dec(edited_json)
453 if data:
454 self._cmd.poutput_json(data)
455
456
Harald Welteee3501f2021-04-02 13:00:18 +0200457 def __init__(self, fid:str, sfid:str=None, name:str=None, desc:str=None, parent:CardDF=None,
458 size={1,None}):
459 """
460 Args:
461 fid : File Identifier (4 hex digits)
462 sfid : Short File Identifier (2 hex digits, optional)
463 name : Brief name of the file, lik EF_ICCID
464 desc : Descriptoin of the file
465 parent : Parent CardFile object within filesystem hierarchy
466 size : tuple of (minimum_size, recommended_size)
467 """
Harald Welteb2edd142021-01-08 23:29:35 +0100468 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent)
469 self.size = size
470 self.shell_commands = [self.ShellCommands()]
471
Harald Welteee3501f2021-04-02 13:00:18 +0200472 def decode_bin(self, raw_bin_data:bytearray) -> dict:
473 """Decode raw (binary) data into abstract representation.
474
475 A derived class would typically provide a _decode_bin() or _decode_hex() method
476 for implementing this specifically for the given file. This function checks which
477 of the method exists, add calls them (with conversion, as needed).
478
479 Args:
480 raw_bin_data : binary encoded data
481 Returns:
482 abstract_data; dict representing the decoded data
483 """
Harald Welteb2edd142021-01-08 23:29:35 +0100484 method = getattr(self, '_decode_bin', None)
485 if callable(method):
486 return method(raw_bin_data)
487 method = getattr(self, '_decode_hex', None)
488 if callable(method):
489 return method(b2h(raw_bin_data))
490 return {'raw': raw_bin_data.hex()}
491
Harald Welteee3501f2021-04-02 13:00:18 +0200492 def decode_hex(self, raw_hex_data:str) -> dict:
493 """Decode raw (hex string) data into abstract representation.
494
495 A derived class would typically provide a _decode_bin() or _decode_hex() method
496 for implementing this specifically for the given file. This function checks which
497 of the method exists, add calls them (with conversion, as needed).
498
499 Args:
500 raw_hex_data : hex-encoded data
501 Returns:
502 abstract_data; dict representing the decoded data
503 """
Harald Welteb2edd142021-01-08 23:29:35 +0100504 method = getattr(self, '_decode_hex', None)
505 if callable(method):
506 return method(raw_hex_data)
507 raw_bin_data = h2b(raw_hex_data)
508 method = getattr(self, '_decode_bin', None)
509 if callable(method):
510 return method(raw_bin_data)
511 return {'raw': raw_bin_data.hex()}
512
Harald Welteee3501f2021-04-02 13:00:18 +0200513 def encode_bin(self, abstract_data:dict) -> bytearray:
514 """Encode abstract representation into raw (binary) data.
515
516 A derived class would typically provide an _encode_bin() or _encode_hex() method
517 for implementing this specifically for the given file. This function checks which
518 of the method exists, add calls them (with conversion, as needed).
519
520 Args:
521 abstract_data : dict representing the decoded data
522 Returns:
523 binary encoded data
524 """
Harald Welteb2edd142021-01-08 23:29:35 +0100525 method = getattr(self, '_encode_bin', None)
526 if callable(method):
527 return method(abstract_data)
528 method = getattr(self, '_encode_hex', None)
529 if callable(method):
530 return h2b(method(abstract_data))
531 raise NotImplementedError
532
Harald Welteee3501f2021-04-02 13:00:18 +0200533 def encode_hex(self, abstract_data:dict) -> str:
534 """Encode abstract representation into raw (hex string) data.
535
536 A derived class would typically provide an _encode_bin() or _encode_hex() method
537 for implementing this specifically for the given file. This function checks which
538 of the method exists, add calls them (with conversion, as needed).
539
540 Args:
541 abstract_data : dict representing the decoded data
542 Returns:
543 hex string encoded data
544 """
Harald Welteb2edd142021-01-08 23:29:35 +0100545 method = getattr(self, '_encode_hex', None)
546 if callable(method):
547 return method(abstract_data)
548 method = getattr(self, '_encode_bin', None)
549 if callable(method):
550 raw_bin_data = method(abstract_data)
551 return b2h(raw_bin_data)
552 raise NotImplementedError
553
554
555class LinFixedEF(CardEF):
Harald Welteee3501f2021-04-02 13:00:18 +0200556 """Linear Fixed EF (Entry File) in the smart card filesystem.
557
558 Linear Fixed EFs are record oriented files. They consist of a number of fixed-size
559 records. The records can be individually read/updated."""
Harald Welteb2edd142021-01-08 23:29:35 +0100560
561 @with_default_category('Linear Fixed EF Commands')
562 class ShellCommands(CommandSet):
Harald Welteee3501f2021-04-02 13:00:18 +0200563 """Shell commands specific for Linear Fixed EFs."""
Harald Welteb2edd142021-01-08 23:29:35 +0100564 def __init__(self):
565 super().__init__()
566
567 read_rec_parser = argparse.ArgumentParser()
568 read_rec_parser.add_argument('record_nr', type=int, help='Number of record to be read')
Philipp Maier41555732021-02-25 16:52:08 +0100569 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 +0100570 @cmd2.with_argparser(read_rec_parser)
571 def do_read_record(self, opts):
Philipp Maier41555732021-02-25 16:52:08 +0100572 """Read one or multiple records from a record-oriented EF"""
573 for r in range(opts.count):
574 recnr = opts.record_nr + r
575 (data, sw) = self._cmd.rs.read_record(recnr)
576 if (len(data) > 0):
577 recstr = str(data)
578 else:
579 recstr = "(empty)"
580 self._cmd.poutput("%03d %s" % (recnr, recstr))
Harald Welteb2edd142021-01-08 23:29:35 +0100581
582 read_rec_dec_parser = argparse.ArgumentParser()
583 read_rec_dec_parser.add_argument('record_nr', type=int, help='Number of record to be read')
Harald Weltebcad86c2021-04-06 20:08:39 +0200584 read_rec_dec_parser.add_argument('--oneline', action='store_true',
585 help='No JSON pretty-printing, dump as a single line')
Harald Welteb2edd142021-01-08 23:29:35 +0100586 @cmd2.with_argparser(read_rec_dec_parser)
587 def do_read_record_decoded(self, opts):
588 """Read + decode a record from a record-oriented EF"""
589 (data, sw) = self._cmd.rs.read_record_dec(opts.record_nr)
Harald Welte1748b932021-04-06 21:12:25 +0200590 self._cmd.poutput_json(data, opts.oneline)
Harald Welteb2edd142021-01-08 23:29:35 +0100591
Harald Welte850b72a2021-04-07 09:33:03 +0200592 read_recs_parser = argparse.ArgumentParser()
593 @cmd2.with_argparser(read_recs_parser)
594 def do_read_records(self, opts):
595 """Read all records from a record-oriented EF"""
596 num_of_rec = self._cmd.rs.selected_file_fcp['file_descriptor']['num_of_rec']
597 for recnr in range(1, 1 + num_of_rec):
598 (data, sw) = self._cmd.rs.read_record(recnr)
599 if (len(data) > 0):
600 recstr = str(data)
601 else:
602 recstr = "(empty)"
603 self._cmd.poutput("%03d %s" % (recnr, recstr))
604
605 read_recs_dec_parser = argparse.ArgumentParser()
606 read_recs_dec_parser.add_argument('--oneline', action='store_true',
607 help='No JSON pretty-printing, dump as a single line')
608 @cmd2.with_argparser(read_recs_dec_parser)
609 def do_read_records_decoded(self, opts):
610 """Read + decode all records from a record-oriented EF"""
611 num_of_rec = self._cmd.rs.selected_file_fcp['file_descriptor']['num_of_rec']
612 # collect all results in list so they are rendered as JSON list when printing
613 data_list = []
614 for recnr in range(1, 1 + num_of_rec):
615 (data, sw) = self._cmd.rs.read_record_dec(recnr)
616 data_list.append(data)
617 self._cmd.poutput_json(data_list, opts.oneline)
618
Harald Welteb2edd142021-01-08 23:29:35 +0100619 upd_rec_parser = argparse.ArgumentParser()
620 upd_rec_parser.add_argument('record_nr', type=int, help='Number of record to be read')
621 upd_rec_parser.add_argument('data', help='Data bytes (hex format) to write')
622 @cmd2.with_argparser(upd_rec_parser)
623 def do_update_record(self, opts):
624 """Update (write) data to a record-oriented EF"""
625 (data, sw) = self._cmd.rs.update_record(opts.record_nr, opts.data)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100626 if data:
627 self._cmd.poutput(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100628
629 upd_rec_dec_parser = argparse.ArgumentParser()
630 upd_rec_dec_parser.add_argument('record_nr', type=int, help='Number of record to be read')
631 upd_rec_dec_parser.add_argument('data', help='Data bytes (hex format) to write')
Harald Welte0d4e98a2021-04-07 00:14:40 +0200632 upd_rec_dec_parser.add_argument('--json-path', type=str,
633 help='JSON path to modify specific element of record only')
Harald Welteb2edd142021-01-08 23:29:35 +0100634 @cmd2.with_argparser(upd_rec_dec_parser)
635 def do_update_record_decoded(self, opts):
636 """Encode + Update (write) data to a record-oriented EF"""
Harald Welte0d4e98a2021-04-07 00:14:40 +0200637 if opts.json_path:
638 (data_json, sw) = self._cmd.rs.read_record_dec(opts.record_nr)
639 js_path_modify(data_json, opts.json_path, json.loads(opts.data))
640 else:
641 data_json = json.loads(opts.data)
642 (data, sw) = self._cmd.rs.update_record_dec(opts.record_nr, data_json)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100643 if data:
644 self._cmd.poutput(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100645
Harald Welte4145d3c2021-04-08 20:34:13 +0200646 edit_rec_dec_parser = argparse.ArgumentParser()
647 edit_rec_dec_parser.add_argument('record_nr', type=int, help='Number of record to be edited')
648 @cmd2.with_argparser(edit_rec_dec_parser)
649 def do_edit_record_decoded(self, opts):
650 """Edit the JSON representation of one record in an editor."""
651 (orig_json, sw) = self._cmd.rs.read_record_dec(opts.record_nr)
652 dirname = tempfile.mkdtemp(prefix='pysim_')
653 try:
654 filename = '%s/file' % dirname
655 # write existing data as JSON to file
656 with open(filename, 'w') as text_file:
657 json.dump(orig_json, text_file, indent=4)
658 # run a text editor
659 self._cmd._run_editor(filename)
660 with open(filename, 'r') as text_file:
661 edited_json = json.load(text_file)
662 if edited_json == orig_json:
663 self._cmd.poutput("Data not modified, skipping write")
664 else:
665 (data, sw) = self._cmd.rs.update_record_dec(opts.record_nr, edited_json)
666 if data:
667 self._cmd.poutput_json(data)
668 finally:
669 shutil.rmtree(dirname)
670
671
Harald Welteee3501f2021-04-02 13:00:18 +0200672 def __init__(self, fid:str, sfid:str=None, name:str=None, desc:str=None,
673 parent:Optional[CardDF]=None, rec_len={1,None}):
674 """
675 Args:
676 fid : File Identifier (4 hex digits)
677 sfid : Short File Identifier (2 hex digits, optional)
678 name : Brief name of the file, lik EF_ICCID
679 desc : Descriptoin of the file
680 parent : Parent CardFile object within filesystem hierarchy
681 rec_len : tuple of (minimum_length, recommended_length)
682 """
Harald Welteb2edd142021-01-08 23:29:35 +0100683 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent)
684 self.rec_len = rec_len
685 self.shell_commands = [self.ShellCommands()]
686
Harald Welteee3501f2021-04-02 13:00:18 +0200687 def decode_record_hex(self, raw_hex_data:str) -> dict:
688 """Decode raw (hex string) data into abstract representation.
689
690 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
691 method for implementing this specifically for the given file. This function checks which
692 of the method exists, add calls them (with conversion, as needed).
693
694 Args:
695 raw_hex_data : hex-encoded data
696 Returns:
697 abstract_data; dict representing the decoded data
698 """
Harald Welteb2edd142021-01-08 23:29:35 +0100699 method = getattr(self, '_decode_record_hex', None)
700 if callable(method):
701 return method(raw_hex_data)
702 raw_bin_data = h2b(raw_hex_data)
703 method = getattr(self, '_decode_record_bin', None)
704 if callable(method):
705 return method(raw_bin_data)
706 return {'raw': raw_bin_data.hex()}
707
Harald Welteee3501f2021-04-02 13:00:18 +0200708 def decode_record_bin(self, raw_bin_data:bytearray) -> dict:
709 """Decode raw (binary) data into abstract representation.
710
711 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
712 method for implementing this specifically for the given file. This function checks which
713 of the method exists, add calls them (with conversion, as needed).
714
715 Args:
716 raw_bin_data : binary encoded data
717 Returns:
718 abstract_data; dict representing the decoded data
719 """
Harald Welteb2edd142021-01-08 23:29:35 +0100720 method = getattr(self, '_decode_record_bin', None)
721 if callable(method):
722 return method(raw_bin_data)
723 raw_hex_data = b2h(raw_bin_data)
724 method = getattr(self, '_decode_record_hex', None)
725 if callable(method):
726 return method(raw_hex_data)
727 return {'raw': raw_hex_data}
728
Harald Welteee3501f2021-04-02 13:00:18 +0200729 def encode_record_hex(self, abstract_data:dict) -> str:
730 """Encode abstract representation into raw (hex string) data.
731
732 A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
733 method for implementing this specifically for the given file. This function checks which
734 of the method exists, add calls them (with conversion, as needed).
735
736 Args:
737 abstract_data : dict representing the decoded data
738 Returns:
739 hex string encoded data
740 """
Harald Welteb2edd142021-01-08 23:29:35 +0100741 method = getattr(self, '_encode_record_hex', None)
742 if callable(method):
743 return method(abstract_data)
744 method = getattr(self, '_encode_record_bin', None)
745 if callable(method):
746 raw_bin_data = method(abstract_data)
Harald Welte1e456572021-04-02 17:16:30 +0200747 return b2h(raw_bin_data)
Harald Welteb2edd142021-01-08 23:29:35 +0100748 raise NotImplementedError
749
Harald Welteee3501f2021-04-02 13:00:18 +0200750 def encode_record_bin(self, abstract_data:dict) -> bytearray:
751 """Encode abstract representation into raw (binary) data.
752
753 A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
754 method for implementing this specifically for the given file. This function checks which
755 of the method exists, add calls them (with conversion, as needed).
756
757 Args:
758 abstract_data : dict representing the decoded data
759 Returns:
760 binary encoded data
761 """
Harald Welteb2edd142021-01-08 23:29:35 +0100762 method = getattr(self, '_encode_record_bin', None)
763 if callable(method):
764 return method(abstract_data)
765 method = getattr(self, '_encode_record_hex', None)
766 if callable(method):
Harald Welteee3501f2021-04-02 13:00:18 +0200767 return h2b(method(abstract_data))
Harald Welteb2edd142021-01-08 23:29:35 +0100768 raise NotImplementedError
769
770class CyclicEF(LinFixedEF):
771 """Cyclic EF (Entry File) in the smart card filesystem"""
772 # we don't really have any special support for those; just recycling LinFixedEF here
Harald Welteee3501f2021-04-02 13:00:18 +0200773 def __init__(self, fid:str, sfid:str=None, name:str=None, desc:str=None, parent:CardDF=None,
774 rec_len={1,None}):
Harald Welteb2edd142021-01-08 23:29:35 +0100775 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, rec_len=rec_len)
776
777class TransRecEF(TransparentEF):
778 """Transparent EF (Entry File) containing fixed-size records.
Harald Welteee3501f2021-04-02 13:00:18 +0200779
Harald Welteb2edd142021-01-08 23:29:35 +0100780 These are the real odd-balls and mostly look like mistakes in the specification:
781 Specified as 'transparent' EF, but actually containing several fixed-length records
782 inside.
783 We add a special class for those, so the user only has to provide encoder/decoder functions
784 for a record, while this class takes care of split / merge of records.
785 """
Harald Welte1e456572021-04-02 17:16:30 +0200786 def __init__(self, fid:str, rec_len:int, sfid:str=None, name:str=None, desc:str=None,
787 parent:Optional[CardDF]=None, size={1,None}):
Harald Welteee3501f2021-04-02 13:00:18 +0200788 """
789 Args:
790 fid : File Identifier (4 hex digits)
791 sfid : Short File Identifier (2 hex digits, optional)
792 name : Brief name of the file, lik EF_ICCID
793 desc : Descriptoin of the file
794 parent : Parent CardFile object within filesystem hierarchy
795 rec_len : Length of the fixed-length records within transparent EF
796 size : tuple of (minimum_size, recommended_size)
797 """
Harald Welteb2edd142021-01-08 23:29:35 +0100798 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, size=size)
799 self.rec_len = rec_len
800
Harald Welteee3501f2021-04-02 13:00:18 +0200801 def decode_record_hex(self, raw_hex_data:str) -> dict:
802 """Decode raw (hex string) data into abstract representation.
803
804 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
805 method for implementing this specifically for the given file. This function checks which
806 of the method exists, add calls them (with conversion, as needed).
807
808 Args:
809 raw_hex_data : hex-encoded data
810 Returns:
811 abstract_data; dict representing the decoded data
812 """
Harald Welteb2edd142021-01-08 23:29:35 +0100813 method = getattr(self, '_decode_record_hex', None)
814 if callable(method):
815 return method(raw_hex_data)
816 method = getattr(self, '_decode_record_bin', None)
817 if callable(method):
818 raw_bin_data = h2b(raw_hex_data)
819 return method(raw_bin_data)
820 return {'raw': raw_hex_data}
821
Harald Welteee3501f2021-04-02 13:00:18 +0200822 def decode_record_bin(self, raw_bin_data:bytearray) -> dict:
823 """Decode raw (binary) data into abstract representation.
824
825 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
826 method for implementing this specifically for the given file. This function checks which
827 of the method exists, add calls them (with conversion, as needed).
828
829 Args:
830 raw_bin_data : binary encoded data
831 Returns:
832 abstract_data; dict representing the decoded data
833 """
Harald Welteb2edd142021-01-08 23:29:35 +0100834 method = getattr(self, '_decode_record_bin', None)
835 if callable(method):
836 return method(raw_bin_data)
837 raw_hex_data = b2h(raw_bin_data)
838 method = getattr(self, '_decode_record_hex', None)
839 if callable(method):
840 return method(raw_hex_data)
841 return {'raw': raw_hex_data}
842
Harald Welteee3501f2021-04-02 13:00:18 +0200843 def encode_record_hex(self, abstract_data:dict) -> str:
844 """Encode abstract representation into raw (hex string) data.
845
846 A derived class would typically provide an _encode_record_bin() or _encode_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 abstract_data : dict representing the decoded data
852 Returns:
853 hex string encoded data
854 """
Harald Welteb2edd142021-01-08 23:29:35 +0100855 method = getattr(self, '_encode_record_hex', None)
856 if callable(method):
857 return method(abstract_data)
858 method = getattr(self, '_encode_record_bin', None)
859 if callable(method):
Harald Welte1e456572021-04-02 17:16:30 +0200860 return b2h(method(abstract_data))
Harald Welteb2edd142021-01-08 23:29:35 +0100861 raise NotImplementedError
862
Harald Welteee3501f2021-04-02 13:00:18 +0200863 def encode_record_bin(self, abstract_data:dict) -> bytearray:
864 """Encode abstract representation into raw (binary) data.
865
866 A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
867 method for implementing this specifically for the given file. This function checks which
868 of the method exists, add calls them (with conversion, as needed).
869
870 Args:
871 abstract_data : dict representing the decoded data
872 Returns:
873 binary encoded data
874 """
Harald Welteb2edd142021-01-08 23:29:35 +0100875 method = getattr(self, '_encode_record_bin', None)
876 if callable(method):
877 return method(abstract_data)
878 method = getattr(self, '_encode_record_hex', None)
879 if callable(method):
880 return h2b(method(abstract_data))
881 raise NotImplementedError
882
Harald Welteee3501f2021-04-02 13:00:18 +0200883 def _decode_bin(self, raw_bin_data:bytearray):
Harald Welteb2edd142021-01-08 23:29:35 +0100884 chunks = [raw_bin_data[i:i+self.rec_len] for i in range(0, len(raw_bin_data), self.rec_len)]
885 return [self.decode_record_bin(x) for x in chunks]
886
Harald Welteee3501f2021-04-02 13:00:18 +0200887 def _encode_bin(self, abstract_data) -> bytes:
Harald Welteb2edd142021-01-08 23:29:35 +0100888 chunks = [self.encode_record_bin(x) for x in abstract_data]
889 # FIXME: pad to file size
890 return b''.join(chunks)
891
892
893
894
895
896class RuntimeState(object):
897 """Represent the runtime state of a session with a card."""
Harald Welteee3501f2021-04-02 13:00:18 +0200898 def __init__(self, card, profile:'CardProfile'):
899 """
900 Args:
901 card : pysim.cards.Card instance
902 profile : CardProfile instance
903 """
Harald Welteb2edd142021-01-08 23:29:35 +0100904 self.mf = CardMF()
905 self.card = card
Harald Weltefe8a7442021-04-10 11:51:54 +0200906 self.selected_file = self.mf # type: CardDF
Harald Welteb2edd142021-01-08 23:29:35 +0100907 self.profile = profile
Harald Welte5ce35242021-04-02 20:27:05 +0200908 # add application ADFs + MF-files from profile
Philipp Maier1e896f32021-03-10 17:02:53 +0100909 apps = self._match_applications()
910 for a in apps:
Harald Welte5ce35242021-04-02 20:27:05 +0200911 if a.adf:
912 self.mf.add_application_df(a.adf)
Harald Welteb2edd142021-01-08 23:29:35 +0100913 for f in self.profile.files_in_mf:
914 self.mf.add_file(f)
Philipp Maier38c74f62021-03-17 17:19:52 +0100915 self.conserve_write = True
Harald Welteb2edd142021-01-08 23:29:35 +0100916
Philipp Maier1e896f32021-03-10 17:02:53 +0100917 def _match_applications(self):
918 """match the applications from the profile with applications on the card"""
919 apps_profile = self.profile.applications
920 aids_card = self.card.read_aids()
921 apps_taken = []
922 if aids_card:
923 aids_taken = []
924 print("AIDs on card:")
925 for a in aids_card:
926 for f in apps_profile:
927 if f.aid in a:
928 print(" %s: %s" % (f.name, a))
929 aids_taken.append(a)
930 apps_taken.append(f)
931 aids_unknown = set(aids_card) - set(aids_taken)
932 for a in aids_unknown:
933 print(" unknown: %s" % a)
934 else:
935 print("error: could not determine card applications")
936 return apps_taken
937
Harald Welteee3501f2021-04-02 13:00:18 +0200938 def get_cwd(self) -> CardDF:
939 """Obtain the current working directory.
940
941 Returns:
942 CardDF instance
943 """
Harald Welteb2edd142021-01-08 23:29:35 +0100944 if isinstance(self.selected_file, CardDF):
945 return self.selected_file
946 else:
947 return self.selected_file.parent
948
Harald Welte5ce35242021-04-02 20:27:05 +0200949 def get_application_df(self) -> Optional[CardADF]:
950 """Obtain the currently selected application DF (if any).
Harald Welteee3501f2021-04-02 13:00:18 +0200951
952 Returns:
953 CardADF() instance or None"""
Harald Welteb2edd142021-01-08 23:29:35 +0100954 # iterate upwards from selected file; check if any is an ADF
955 node = self.selected_file
956 while node.parent != node:
957 if isinstance(node, CardADF):
958 return node
959 node = node.parent
960 return None
961
Harald Welteee3501f2021-04-02 13:00:18 +0200962 def interpret_sw(self, sw:str):
963 """Interpret a given status word relative to the currently selected application
964 or the underlying card profile.
965
966 Args:
967 sw : Status word as string of 4 hexd digits
968
969 Returns:
970 Tuple of two strings
971 """
Harald Welte86fbd392021-04-02 22:13:09 +0200972 res = None
Harald Welte5ce35242021-04-02 20:27:05 +0200973 adf = self.get_application_df()
974 if adf:
975 app = adf.application
Harald Welteb2edd142021-01-08 23:29:35 +0100976 # The application either comes with its own interpret_sw
977 # method or we will use the interpret_sw method from the
978 # card profile.
Harald Welte5ce35242021-04-02 20:27:05 +0200979 if app and hasattr(app, "interpret_sw"):
Harald Welte86fbd392021-04-02 22:13:09 +0200980 res = app.interpret_sw(sw)
981 return res or self.profile.interpret_sw(sw)
Harald Welteb2edd142021-01-08 23:29:35 +0100982
Harald Welteee3501f2021-04-02 13:00:18 +0200983 def probe_file(self, fid:str, cmd_app=None):
984 """Blindly try to select a file and automatically add a matching file
985 object if the file actually exists."""
Philipp Maier63f572d2021-03-09 22:42:47 +0100986 if not is_hex(fid, 4, 4):
987 raise ValueError("Cannot select unknown file by name %s, only hexadecimal 4 digit FID is allowed" % fid)
988
989 try:
990 (data, sw) = self.card._scc.select_file(fid)
991 except SwMatchError as swm:
992 k = self.interpret_sw(swm.sw_actual)
993 if not k:
994 raise(swm)
995 raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1]))
996
997 select_resp = self.selected_file.decode_select_response(data)
998 if (select_resp['file_descriptor']['file_type'] == 'df'):
999 f = CardDF(fid=fid, sfid=None, name="DF." + str(fid).upper(), desc="dedicated file, manually added at runtime")
1000 else:
1001 if (select_resp['file_descriptor']['structure'] == 'transparent'):
1002 f = TransparentEF(fid=fid, sfid=None, name="EF." + str(fid).upper(), desc="elementry file, manually added at runtime")
1003 else:
1004 f = LinFixedEF(fid=fid, sfid=None, name="EF." + str(fid).upper(), desc="elementry file, manually added at runtime")
1005
1006 self.selected_file.add_files([f])
1007 self.selected_file = f
1008 return select_resp
1009
Harald Welteee3501f2021-04-02 13:00:18 +02001010 def select(self, name:str, cmd_app=None):
1011 """Select a file (EF, DF, ADF, MF, ...).
1012
1013 Args:
1014 name : Name of file to select
1015 cmd_app : Command Application State (for unregistering old file commands)
1016 """
Harald Welteb2edd142021-01-08 23:29:35 +01001017 sels = self.selected_file.get_selectables()
Philipp Maier7744b6e2021-03-11 14:29:37 +01001018 if is_hex(name):
1019 name = name.lower()
Philipp Maier63f572d2021-03-09 22:42:47 +01001020
1021 # unregister commands of old file
1022 if cmd_app and self.selected_file.shell_commands:
1023 for c in self.selected_file.shell_commands:
1024 cmd_app.unregister_command_set(c)
1025
Harald Welteb2edd142021-01-08 23:29:35 +01001026 if name in sels:
1027 f = sels[name]
Harald Welteb2edd142021-01-08 23:29:35 +01001028 try:
1029 if isinstance(f, CardADF):
Philipp Maiercba6dbc2021-03-11 13:03:18 +01001030 (data, sw) = self.card.select_adf_by_aid(f.aid)
Harald Welteb2edd142021-01-08 23:29:35 +01001031 else:
1032 (data, sw) = self.card._scc.select_file(f.fid)
1033 self.selected_file = f
1034 except SwMatchError as swm:
1035 k = self.interpret_sw(swm.sw_actual)
1036 if not k:
1037 raise(swm)
1038 raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1]))
Philipp Maier63f572d2021-03-09 22:42:47 +01001039 select_resp = f.decode_select_response(data)
Harald Welteb2edd142021-01-08 23:29:35 +01001040 else:
Philipp Maier63f572d2021-03-09 22:42:47 +01001041 select_resp = self.probe_file(name, cmd_app)
Harald Welte850b72a2021-04-07 09:33:03 +02001042 # store the decoded FCP for later reference
1043 self.selected_file_fcp = select_resp
Philipp Maier63f572d2021-03-09 22:42:47 +01001044
1045 # register commands of new file
1046 if cmd_app and self.selected_file.shell_commands:
1047 for c in self.selected_file.shell_commands:
1048 cmd_app.register_command_set(c)
1049
1050 return select_resp
Harald Welteb2edd142021-01-08 23:29:35 +01001051
Harald Welteee3501f2021-04-02 13:00:18 +02001052 def read_binary(self, length:int=None, offset:int=0):
1053 """Read [part of] a transparent EF binary data.
1054
1055 Args:
1056 length : Amount of data to read (None: as much as possible)
1057 offset : Offset into the file from which to read 'length' bytes
1058 Returns:
1059 binary data read from the file
1060 """
Harald Welteb2edd142021-01-08 23:29:35 +01001061 if not isinstance(self.selected_file, TransparentEF):
1062 raise TypeError("Only works with TransparentEF")
1063 return self.card._scc.read_binary(self.selected_file.fid, length, offset)
1064
Harald Welte2d4a64b2021-04-03 09:01:24 +02001065 def read_binary_dec(self) -> Tuple[dict, str]:
Harald Welteee3501f2021-04-02 13:00:18 +02001066 """Read [part of] a transparent EF binary data and decode it.
1067
1068 Args:
1069 length : Amount of data to read (None: as much as possible)
1070 offset : Offset into the file from which to read 'length' bytes
1071 Returns:
1072 abstract decode data read from the file
1073 """
Harald Welteb2edd142021-01-08 23:29:35 +01001074 (data, sw) = self.read_binary()
1075 dec_data = self.selected_file.decode_hex(data)
Harald Welteb2edd142021-01-08 23:29:35 +01001076 return (dec_data, sw)
1077
Harald Welteee3501f2021-04-02 13:00:18 +02001078 def update_binary(self, data_hex:str, offset:int=0):
1079 """Update transparent EF binary data.
1080
1081 Args:
1082 data_hex : hex string of data to be written
1083 offset : Offset into the file from which to write 'data_hex'
1084 """
Harald Welteb2edd142021-01-08 23:29:35 +01001085 if not isinstance(self.selected_file, TransparentEF):
1086 raise TypeError("Only works with TransparentEF")
Philipp Maier38c74f62021-03-17 17:19:52 +01001087 return self.card._scc.update_binary(self.selected_file.fid, data_hex, offset, conserve=self.conserve_write)
Harald Welteb2edd142021-01-08 23:29:35 +01001088
Harald Welteee3501f2021-04-02 13:00:18 +02001089 def update_binary_dec(self, data:dict):
1090 """Update transparent EF from abstract data. Encodes the data to binary and
1091 then updates the EF with it.
1092
1093 Args:
1094 data : abstract data which is to be encoded and written
1095 """
Harald Welteb2edd142021-01-08 23:29:35 +01001096 data_hex = self.selected_file.encode_hex(data)
Harald Welteb2edd142021-01-08 23:29:35 +01001097 return self.update_binary(data_hex)
1098
Harald Welteee3501f2021-04-02 13:00:18 +02001099 def read_record(self, rec_nr:int=0):
1100 """Read a record as binary data.
1101
1102 Args:
1103 rec_nr : Record number to read
1104 Returns:
1105 hex string of binary data contained in record
1106 """
Harald Welteb2edd142021-01-08 23:29:35 +01001107 if not isinstance(self.selected_file, LinFixedEF):
1108 raise TypeError("Only works with Linear Fixed EF")
1109 # returns a string of hex nibbles
1110 return self.card._scc.read_record(self.selected_file.fid, rec_nr)
1111
Harald Welteee3501f2021-04-02 13:00:18 +02001112 def read_record_dec(self, rec_nr:int=0) -> Tuple[dict, str]:
1113 """Read a record and decode it to abstract data.
1114
1115 Args:
1116 rec_nr : Record number to read
1117 Returns:
1118 abstract data contained in record
1119 """
Harald Welteb2edd142021-01-08 23:29:35 +01001120 (data, sw) = self.read_record(rec_nr)
1121 return (self.selected_file.decode_record_hex(data), sw)
1122
Harald Welteee3501f2021-04-02 13:00:18 +02001123 def update_record(self, rec_nr:int, data_hex:str):
1124 """Update a record with given binary data
1125
1126 Args:
1127 rec_nr : Record number to read
1128 data_hex : Hex string binary data to be written
1129 """
Harald Welteb2edd142021-01-08 23:29:35 +01001130 if not isinstance(self.selected_file, LinFixedEF):
1131 raise TypeError("Only works with Linear Fixed EF")
Philipp Maier38c74f62021-03-17 17:19:52 +01001132 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 +01001133
Harald Welteee3501f2021-04-02 13:00:18 +02001134 def update_record_dec(self, rec_nr:int, data:dict):
1135 """Update a record with given abstract data. Will encode abstract to binary data
1136 and then write it to the given record on the card.
1137
1138 Args:
1139 rec_nr : Record number to read
1140 data_hex : Abstract data to be written
1141 """
Harald Welte1e456572021-04-02 17:16:30 +02001142 data_hex = self.selected_file.encode_record_hex(data)
1143 return self.update_record(rec_nr, data_hex)
Harald Welteb2edd142021-01-08 23:29:35 +01001144
1145
1146
1147class FileData(object):
1148 """Represent the runtime, on-card data."""
1149 def __init__(self, fdesc):
1150 self.desc = fdesc
1151 self.fcp = None
1152
1153
Harald Welteee3501f2021-04-02 13:00:18 +02001154def interpret_sw(sw_data:dict, sw:str):
1155 """Interpret a given status word.
1156
1157 Args:
1158 sw_data : Hierarchical dict of status word matches
1159 sw : status word to match (string of 4 hex digits)
1160 Returns:
1161 tuple of two strings (class_string, description)
1162 """
Harald Welteb2edd142021-01-08 23:29:35 +01001163 for class_str, swdict in sw_data.items():
1164 # first try direct match
1165 if sw in swdict:
1166 return (class_str, swdict[sw])
1167 # next try wildcard matches
1168 for pattern, descr in swdict.items():
1169 if sw_match(sw, pattern):
1170 return (class_str, descr)
1171 return None
1172
1173class CardApplication(object):
1174 """A card application is represented by an ADF (with contained hierarchy) and optionally
1175 some SW definitions."""
Harald Welte5ce35242021-04-02 20:27:05 +02001176 def __init__(self, name, adf:Optional[CardADF]=None, aid:str=None, sw:dict=None):
Harald Welteee3501f2021-04-02 13:00:18 +02001177 """
1178 Args:
1179 adf : ADF name
1180 sw : Dict of status word conversions
1181 """
Harald Welteb2edd142021-01-08 23:29:35 +01001182 self.name = name
1183 self.adf = adf
Vadim Yanitskiy98f872b2021-03-27 01:25:46 +01001184 self.sw = sw or dict()
Harald Welte5ce35242021-04-02 20:27:05 +02001185 # back-reference from ADF to Applicaiton
1186 if self.adf:
1187 self.aid = aid or self.adf.aid
1188 self.adf.application = self
1189 else:
1190 self.aid = aid
Harald Welteb2edd142021-01-08 23:29:35 +01001191
1192 def __str__(self):
1193 return "APP(%s)" % (self.name)
1194
1195 def interpret_sw(self, sw):
Harald Welteee3501f2021-04-02 13:00:18 +02001196 """Interpret a given status word within the application.
1197
1198 Args:
1199 sw : Status word as string of 4 hexd digits
1200
1201 Returns:
1202 Tuple of two strings
1203 """
Harald Welteb2edd142021-01-08 23:29:35 +01001204 return interpret_sw(self.sw, sw)
1205
1206class CardProfile(object):
1207 """A Card Profile describes a card, it's filessystem hierarchy, an [initial] list of
1208 applications as well as profile-specific SW and shell commands. Every card has
1209 one card profile, but there may be multiple applications within that profile."""
Vadim Yanitskiy98f872b2021-03-27 01:25:46 +01001210 def __init__(self, name, **kw):
Harald Welteee3501f2021-04-02 13:00:18 +02001211 """
1212 Args:
1213 desc (str) : Description
1214 files_in_mf : List of CardEF instances present in MF
1215 applications : List of CardApplications present on card
1216 sw : List of status word definitions
1217 shell_cmdsets : List of cmd2 shell command sets of profile-specific commands
1218 """
Harald Welteb2edd142021-01-08 23:29:35 +01001219 self.name = name
Vadim Yanitskiy98f872b2021-03-27 01:25:46 +01001220 self.desc = kw.get("desc", None)
1221 self.files_in_mf = kw.get("files_in_mf", [])
1222 self.sw = kw.get("sw", [])
1223 self.applications = kw.get("applications", [])
1224 self.shell_cmdsets = kw.get("shell_cmdsets", [])
Harald Welteb2edd142021-01-08 23:29:35 +01001225
1226 def __str__(self):
1227 return self.name
1228
Harald Welteee3501f2021-04-02 13:00:18 +02001229 def add_application(self, app:CardApplication):
1230 """Add an application to a card profile.
1231
1232 Args:
1233 app : CardApplication instance to be added to profile
1234 """
Philipp Maiereb72fa42021-03-26 21:29:57 +01001235 self.applications.append(app)
Harald Welteb2edd142021-01-08 23:29:35 +01001236
Harald Welteee3501f2021-04-02 13:00:18 +02001237 def interpret_sw(self, sw:str):
1238 """Interpret a given status word within the profile.
1239
1240 Args:
1241 sw : Status word as string of 4 hexd digits
1242
1243 Returns:
1244 Tuple of two strings
1245 """
Harald Welteb2edd142021-01-08 23:29:35 +01001246 return interpret_sw(self.sw, sw)