blob: 097661dfaf1c98e39d7679eccf5444acd0412f7c [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
28import json
29
30import cmd2
31from cmd2 import CommandSet, with_default_category, with_argparser
32import argparse
33
Harald Welte1e456572021-04-02 17:16:30 +020034from typing import cast, Optional, Iterable, List, Any, Dict, Tuple
Harald Welteee3501f2021-04-02 13:00:18 +020035
Philipp Maier3aec8712021-03-09 21:49:01 +010036from pySim.utils import sw_match, h2b, b2h, is_hex
Harald Welteb2edd142021-01-08 23:29:35 +010037from pySim.exceptions import *
Harald Welte0d4e98a2021-04-07 00:14:40 +020038from pySim.jsonpath import js_path_find, js_path_modify
Harald Welteb2edd142021-01-08 23:29:35 +010039
40class CardFile(object):
41 """Base class for all objects in the smart card filesystem.
42 Serve as a common ancestor to all other file types; rarely used directly.
43 """
44 RESERVED_NAMES = ['..', '.', '/', 'MF']
45 RESERVED_FIDS = ['3f00']
46
Harald Welteee3501f2021-04-02 13:00:18 +020047 def __init__(self, fid:str=None, sfid:str=None, name:str=None, desc:str=None,
48 parent:Optional['CardDF']=None):
49 """
50 Args:
51 fid : File Identifier (4 hex digits)
52 sfid : Short File Identifier (2 hex digits, optional)
53 name : Brief name of the file, lik EF_ICCID
54 desc : Descriptoin of the file
55 parent : Parent CardFile object within filesystem hierarchy
56 """
Harald Welteb2edd142021-01-08 23:29:35 +010057 if not isinstance(self, CardADF) and fid == None:
58 raise ValueError("fid is mandatory")
59 if fid:
60 fid = fid.lower()
61 self.fid = fid # file identifier
62 self.sfid = sfid # short file identifier
63 self.name = name # human readable name
64 self.desc = desc # human readable description
65 self.parent = parent
66 if self.parent and self.parent != self and self.fid:
67 self.parent.add_file(self)
Vadim Yanitskiya0040792021-04-09 22:44:44 +020068 self.shell_commands = [] # type: List[CommandSet]
Harald Welteb2edd142021-01-08 23:29:35 +010069
Philipp Maier66061582021-03-09 21:57:57 +010070 # Note: the basic properties (fid, name, ect.) are verified when
71 # the file is attached to a parent file. See method add_file() in
72 # class Card DF
73
Harald Welteb2edd142021-01-08 23:29:35 +010074 def __str__(self):
75 if self.name:
76 return self.name
77 else:
78 return self.fid
79
Harald Welteee3501f2021-04-02 13:00:18 +020080 def _path_element(self, prefer_name:bool) -> Optional[str]:
Harald Welteb2edd142021-01-08 23:29:35 +010081 if prefer_name and self.name:
82 return self.name
83 else:
84 return self.fid
85
Harald Welte1e456572021-04-02 17:16:30 +020086 def fully_qualified_path(self, prefer_name:bool=True) -> List[str]:
Harald Welteee3501f2021-04-02 13:00:18 +020087 """Return fully qualified path to file as list of FID or name strings.
88
89 Args:
90 prefer_name : Preferably build path of names; fall-back to FIDs as required
91 """
Harald Welte1e456572021-04-02 17:16:30 +020092 if self.parent and self.parent != self:
Harald Welteb2edd142021-01-08 23:29:35 +010093 ret = self.parent.fully_qualified_path(prefer_name)
94 else:
95 ret = []
Harald Welte1e456572021-04-02 17:16:30 +020096 elem = self._path_element(prefer_name)
97 if elem:
98 ret.append(elem)
Harald Welteb2edd142021-01-08 23:29:35 +010099 return ret
100
Harald Welteee3501f2021-04-02 13:00:18 +0200101 def get_mf(self) -> Optional['CardMF']:
Harald Welteb2edd142021-01-08 23:29:35 +0100102 """Return the MF (root) of the file system."""
103 if self.parent == None:
104 return None
105 # iterate towards the top. MF has parent == self
106 node = self
Harald Welte1e456572021-04-02 17:16:30 +0200107 while node.parent and node.parent != node:
Harald Welteb2edd142021-01-08 23:29:35 +0100108 node = node.parent
Harald Welte1e456572021-04-02 17:16:30 +0200109 return cast(CardMF, node)
Harald Welteb2edd142021-01-08 23:29:35 +0100110
Harald Welte1e456572021-04-02 17:16:30 +0200111 def _get_self_selectables(self, alias:str=None, flags = []) -> Dict[str, 'CardFile']:
Harald Welteee3501f2021-04-02 13:00:18 +0200112 """Return a dict of {'identifier': self} tuples.
113
114 Args:
115 alias : Add an alias with given name to 'self'
116 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
117 If not specified, all selectables will be returned.
118 Returns:
119 dict containing reference to 'self' for all identifiers.
120 """
Harald Welteb2edd142021-01-08 23:29:35 +0100121 sels = {}
122 if alias:
123 sels.update({alias: self})
Philipp Maier786f7812021-02-25 16:48:10 +0100124 if self.fid and (flags == [] or 'FIDS' in flags):
Harald Welteb2edd142021-01-08 23:29:35 +0100125 sels.update({self.fid: self})
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100126 if self.name and (flags == [] or 'FNAMES' in flags):
Harald Welteb2edd142021-01-08 23:29:35 +0100127 sels.update({self.name: self})
128 return sels
129
Harald Welte1e456572021-04-02 17:16:30 +0200130 def get_selectables(self, flags = []) -> Dict[str, 'CardFile']:
Harald Welteee3501f2021-04-02 13:00:18 +0200131 """Return a dict of {'identifier': File} that is selectable from the current file.
132
133 Args:
134 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
135 If not specified, all selectables will be returned.
136 Returns:
137 dict containing all selectable items. Key is identifier (string), value
138 a reference to a CardFile (or derived class) instance.
139 """
Philipp Maier786f7812021-02-25 16:48:10 +0100140 sels = {}
Harald Welteb2edd142021-01-08 23:29:35 +0100141 # we can always select ourself
Philipp Maier786f7812021-02-25 16:48:10 +0100142 if flags == [] or 'SELF' in flags:
143 sels = self._get_self_selectables('.', flags)
Harald Welteb2edd142021-01-08 23:29:35 +0100144 # we can always select our parent
Philipp Maier786f7812021-02-25 16:48:10 +0100145 if flags == [] or 'PARENT' in flags:
Harald Welte1e456572021-04-02 17:16:30 +0200146 if self.parent:
147 sels = self.parent._get_self_selectables('..', flags)
Harald Welteb2edd142021-01-08 23:29:35 +0100148 # if we have a MF, we can always select its applications
Philipp Maier786f7812021-02-25 16:48:10 +0100149 if flags == [] or 'MF' in flags:
150 mf = self.get_mf()
151 if mf:
152 sels.update(mf._get_self_selectables(flags = flags))
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100153 sels.update(mf.get_app_selectables(flags = flags))
Harald Welteb2edd142021-01-08 23:29:35 +0100154 return sels
155
Harald Welte1e456572021-04-02 17:16:30 +0200156 def get_selectable_names(self, flags = []) -> List[str]:
Harald Welteee3501f2021-04-02 13:00:18 +0200157 """Return a dict of {'identifier': File} that is selectable from the current file.
158
159 Args:
160 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
161 If not specified, all selectables will be returned.
162 Returns:
Harald Welte1e456572021-04-02 17:16:30 +0200163 list containing all selectable names.
Harald Welteee3501f2021-04-02 13:00:18 +0200164 """
Philipp Maier786f7812021-02-25 16:48:10 +0100165 sels = self.get_selectables(flags)
Harald Welte1e456572021-04-02 17:16:30 +0200166 return list(sels.keys())
Harald Welteb2edd142021-01-08 23:29:35 +0100167
Harald Welteee3501f2021-04-02 13:00:18 +0200168 def decode_select_response(self, data_hex:str):
Harald Welteb2edd142021-01-08 23:29:35 +0100169 """Decode the response to a SELECT command."""
Harald Welte1e456572021-04-02 17:16:30 +0200170 if self.parent:
171 return self.parent.decode_select_response(data_hex)
Harald Welteb2edd142021-01-08 23:29:35 +0100172
173
174class CardDF(CardFile):
175 """DF (Dedicated File) in the smart card filesystem. Those are basically sub-directories."""
Philipp Maier63f572d2021-03-09 22:42:47 +0100176
177 @with_default_category('DF/ADF Commands')
178 class ShellCommands(CommandSet):
179 def __init__(self):
180 super().__init__()
181
Harald Welteb2edd142021-01-08 23:29:35 +0100182 def __init__(self, **kwargs):
183 if not isinstance(self, CardADF):
184 if not 'fid' in kwargs:
185 raise TypeError('fid is mandatory for all DF')
186 super().__init__(**kwargs)
187 self.children = dict()
Philipp Maier63f572d2021-03-09 22:42:47 +0100188 self.shell_commands = [self.ShellCommands()]
Harald Welteb2edd142021-01-08 23:29:35 +0100189
190 def __str__(self):
191 return "DF(%s)" % (super().__str__())
192
Harald Welteee3501f2021-04-02 13:00:18 +0200193 def add_file(self, child:CardFile, ignore_existing:bool=False):
194 """Add a child (DF/EF) to this DF.
195 Args:
196 child: The new DF/EF to be added
197 ignore_existing: Ignore, if file with given FID already exists. Old one will be kept.
198 """
Harald Welteb2edd142021-01-08 23:29:35 +0100199 if not isinstance(child, CardFile):
200 raise TypeError("Expected a File instance")
Philipp Maier3aec8712021-03-09 21:49:01 +0100201 if not is_hex(child.fid, minlen = 4, maxlen = 4):
202 raise ValueError("File name %s is not a valid fid" % (child.fid))
Harald Welteb2edd142021-01-08 23:29:35 +0100203 if child.name in CardFile.RESERVED_NAMES:
204 raise ValueError("File name %s is a reserved name" % (child.name))
205 if child.fid in CardFile.RESERVED_FIDS:
Philipp Maiere8bc1b42021-03-09 20:33:41 +0100206 raise ValueError("File fid %s is a reserved fid" % (child.fid))
Harald Welteb2edd142021-01-08 23:29:35 +0100207 if child.fid in self.children:
208 if ignore_existing:
209 return
210 raise ValueError("File with given fid %s already exists" % (child.fid))
211 if self.lookup_file_by_sfid(child.sfid):
212 raise ValueError("File with given sfid %s already exists" % (child.sfid))
213 if self.lookup_file_by_name(child.name):
214 if ignore_existing:
215 return
216 raise ValueError("File with given name %s already exists" % (child.name))
217 self.children[child.fid] = child
218 child.parent = self
219
Harald Welteee3501f2021-04-02 13:00:18 +0200220 def add_files(self, children:Iterable[CardFile], ignore_existing:bool=False):
221 """Add a list of child (DF/EF) to this DF
222
223 Args:
224 children: List of new DF/EFs to be added
225 ignore_existing: Ignore, if file[s] with given FID already exists. Old one[s] will be kept.
226 """
Harald Welteb2edd142021-01-08 23:29:35 +0100227 for child in children:
228 self.add_file(child, ignore_existing)
229
Harald Welteee3501f2021-04-02 13:00:18 +0200230 def get_selectables(self, flags = []) -> dict:
231 """Return a dict of {'identifier': File} that is selectable from the current DF.
232
233 Args:
234 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
235 If not specified, all selectables will be returned.
236 Returns:
237 dict containing all selectable items. Key is identifier (string), value
238 a reference to a CardFile (or derived class) instance.
239 """
Harald Welteb2edd142021-01-08 23:29:35 +0100240 # global selectables + our children
Philipp Maier786f7812021-02-25 16:48:10 +0100241 sels = super().get_selectables(flags)
242 if flags == [] or 'FIDS' in flags:
243 sels.update({x.fid: x for x in self.children.values() if x.fid})
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100244 if flags == [] or 'FNAMES' in flags:
Philipp Maier786f7812021-02-25 16:48:10 +0100245 sels.update({x.name: x for x in self.children.values() if x.name})
Harald Welteb2edd142021-01-08 23:29:35 +0100246 return sels
247
Harald Welte1e456572021-04-02 17:16:30 +0200248 def lookup_file_by_name(self, name:Optional[str]) -> Optional[CardFile]:
Harald Welteee3501f2021-04-02 13:00:18 +0200249 """Find a file with given name within current DF."""
Harald Welteb2edd142021-01-08 23:29:35 +0100250 if name == None:
251 return None
252 for i in self.children.values():
253 if i.name and i.name == name:
254 return i
255 return None
256
Harald Welte1e456572021-04-02 17:16:30 +0200257 def lookup_file_by_sfid(self, sfid:Optional[str]) -> Optional[CardFile]:
Harald Welteee3501f2021-04-02 13:00:18 +0200258 """Find a file with given short file ID within current DF."""
Harald Welteb2edd142021-01-08 23:29:35 +0100259 if sfid == None:
260 return None
261 for i in self.children.values():
Harald Welte1e456572021-04-02 17:16:30 +0200262 if i.sfid == int(str(sfid)):
Harald Welteb2edd142021-01-08 23:29:35 +0100263 return i
264 return None
265
Harald Welteee3501f2021-04-02 13:00:18 +0200266 def lookup_file_by_fid(self, fid:str) -> Optional[CardFile]:
267 """Find a file with given file ID within current DF."""
Harald Welteb2edd142021-01-08 23:29:35 +0100268 if fid in self.children:
269 return self.children[fid]
270 return None
271
272
273class CardMF(CardDF):
274 """MF (Master File) in the smart card filesystem"""
275 def __init__(self, **kwargs):
276 # can be overridden; use setdefault
277 kwargs.setdefault('fid', '3f00')
278 kwargs.setdefault('name', 'MF')
279 kwargs.setdefault('desc', 'Master File (directory root)')
280 # cannot be overridden; use assignment
281 kwargs['parent'] = self
282 super().__init__(**kwargs)
283 self.applications = dict()
284
285 def __str__(self):
286 return "MF(%s)" % (self.fid)
287
Harald Welte5ce35242021-04-02 20:27:05 +0200288 def add_application_df(self, app:'CardADF'):
289 """Add an Application to the MF"""
Harald Welteb2edd142021-01-08 23:29:35 +0100290 if not isinstance(app, CardADF):
291 raise TypeError("Expected an ADF instance")
292 if app.aid in self.applications:
293 raise ValueError("AID %s already exists" % (app.aid))
294 self.applications[app.aid] = app
295 app.parent=self
296
297 def get_app_names(self):
298 """Get list of completions (AID names)"""
299 return [x.name for x in self.applications]
300
Harald Welteee3501f2021-04-02 13:00:18 +0200301 def get_selectables(self, flags = []) -> dict:
302 """Return a dict of {'identifier': File} that is selectable from the current DF.
303
304 Args:
305 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
306 If not specified, all selectables will be returned.
307 Returns:
308 dict containing all selectable items. Key is identifier (string), value
309 a reference to a CardFile (or derived class) instance.
310 """
Philipp Maier786f7812021-02-25 16:48:10 +0100311 sels = super().get_selectables(flags)
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100312 sels.update(self.get_app_selectables(flags))
Harald Welteb2edd142021-01-08 23:29:35 +0100313 return sels
314
Harald Welteee3501f2021-04-02 13:00:18 +0200315 def get_app_selectables(self, flags = []) -> dict:
Philipp Maier786f7812021-02-25 16:48:10 +0100316 """Get applications by AID + name"""
317 sels = {}
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100318 if flags == [] or 'AIDS' in flags:
Philipp Maier786f7812021-02-25 16:48:10 +0100319 sels.update({x.aid: x for x in self.applications.values()})
Philipp Maierbd8ed2c2021-03-18 17:09:33 +0100320 if flags == [] or 'ANAMES' in flags:
Philipp Maier786f7812021-02-25 16:48:10 +0100321 sels.update({x.name: x for x in self.applications.values() if x.name})
Harald Welteb2edd142021-01-08 23:29:35 +0100322 return sels
323
Harald Welteee3501f2021-04-02 13:00:18 +0200324 def decode_select_response(self, data_hex:str) -> Any:
325 """Decode the response to a SELECT command.
326
327 This is the fall-back method which doesn't perform any decoding. It mostly
328 exists so specific derived classes can overload it for actual decoding.
329 """
Harald Welteb2edd142021-01-08 23:29:35 +0100330 return data_hex
331
332
333
334class CardADF(CardDF):
335 """ADF (Application Dedicated File) in the smart card filesystem"""
Harald Welteee3501f2021-04-02 13:00:18 +0200336 def __init__(self, aid:str, **kwargs):
Harald Welteb2edd142021-01-08 23:29:35 +0100337 super().__init__(**kwargs)
Harald Welte5ce35242021-04-02 20:27:05 +0200338 # reference to CardApplication may be set from CardApplication constructor
339 self.application:Optional[CardApplication] = None
Harald Welteb2edd142021-01-08 23:29:35 +0100340 self.aid = aid # Application Identifier
Harald Welte1e456572021-04-02 17:16:30 +0200341 mf = self.get_mf()
342 if mf:
Harald Welte5ce35242021-04-02 20:27:05 +0200343 mf.add_application_df(self)
Harald Welteb2edd142021-01-08 23:29:35 +0100344
345 def __str__(self):
346 return "ADF(%s)" % (self.aid)
347
Harald Welteee3501f2021-04-02 13:00:18 +0200348 def _path_element(self, prefer_name:bool):
Harald Welteb2edd142021-01-08 23:29:35 +0100349 if self.name and prefer_name:
350 return self.name
351 else:
352 return self.aid
353
354
355class CardEF(CardFile):
356 """EF (Entry File) in the smart card filesystem"""
357 def __init__(self, *, fid, **kwargs):
358 kwargs['fid'] = fid
359 super().__init__(**kwargs)
360
361 def __str__(self):
362 return "EF(%s)" % (super().__str__())
363
Harald Welteee3501f2021-04-02 13:00:18 +0200364 def get_selectables(self, flags = []) -> dict:
365 """Return a dict of {'identifier': File} that is selectable from the current DF.
366
367 Args:
368 flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
369 If not specified, all selectables will be returned.
370 Returns:
371 dict containing all selectable items. Key is identifier (string), value
372 a reference to a CardFile (or derived class) instance.
373 """
Harald Welteb2edd142021-01-08 23:29:35 +0100374 #global selectable names + those of the parent DF
Philipp Maier786f7812021-02-25 16:48:10 +0100375 sels = super().get_selectables(flags)
Harald Welteb2edd142021-01-08 23:29:35 +0100376 sels.update({x.name:x for x in self.parent.children.values() if x != self})
377 return sels
378
379
380class TransparentEF(CardEF):
Harald Welteee3501f2021-04-02 13:00:18 +0200381 """Transparent EF (Entry File) in the smart card filesystem.
382
383 A Transparent EF is a binary file with no formal structure. This is contrary to
384 Record based EFs which have [fixed size] records that can be individually read/updated."""
Harald Welteb2edd142021-01-08 23:29:35 +0100385
386 @with_default_category('Transparent EF Commands')
387 class ShellCommands(CommandSet):
Harald Welteee3501f2021-04-02 13:00:18 +0200388 """Shell commands specific for Trransparent EFs."""
Harald Welteb2edd142021-01-08 23:29:35 +0100389 def __init__(self):
390 super().__init__()
391
392 read_bin_parser = argparse.ArgumentParser()
393 read_bin_parser.add_argument('--offset', type=int, default=0, help='Byte offset for start of read')
394 read_bin_parser.add_argument('--length', type=int, help='Number of bytes to read')
395 @cmd2.with_argparser(read_bin_parser)
396 def do_read_binary(self, opts):
397 """Read binary data from a transparent EF"""
398 (data, sw) = self._cmd.rs.read_binary(opts.length, opts.offset)
399 self._cmd.poutput(data)
400
Harald Weltebcad86c2021-04-06 20:08:39 +0200401 read_bin_dec_parser = argparse.ArgumentParser()
402 read_bin_dec_parser.add_argument('--oneline', action='store_true',
403 help='No JSON pretty-printing, dump as a single line')
404 @cmd2.with_argparser(read_bin_dec_parser)
Harald Welteb2edd142021-01-08 23:29:35 +0100405 def do_read_binary_decoded(self, opts):
406 """Read + decode data from a transparent EF"""
407 (data, sw) = self._cmd.rs.read_binary_dec()
Harald Welte1748b932021-04-06 21:12:25 +0200408 self._cmd.poutput_json(data, opts.oneline)
Harald Welteb2edd142021-01-08 23:29:35 +0100409
410 upd_bin_parser = argparse.ArgumentParser()
411 upd_bin_parser.add_argument('--offset', type=int, default=0, help='Byte offset for start of read')
412 upd_bin_parser.add_argument('data', help='Data bytes (hex format) to write')
413 @cmd2.with_argparser(upd_bin_parser)
414 def do_update_binary(self, opts):
415 """Update (Write) data of a transparent EF"""
416 (data, sw) = self._cmd.rs.update_binary(opts.data, opts.offset)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100417 if data:
418 self._cmd.poutput(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100419
420 upd_bin_dec_parser = argparse.ArgumentParser()
421 upd_bin_dec_parser.add_argument('data', help='Abstract data (JSON format) to write')
Harald Welte0d4e98a2021-04-07 00:14:40 +0200422 upd_bin_dec_parser.add_argument('--json-path', type=str,
423 help='JSON path to modify specific element of file only')
Harald Welteb2edd142021-01-08 23:29:35 +0100424 @cmd2.with_argparser(upd_bin_dec_parser)
425 def do_update_binary_decoded(self, opts):
426 """Encode + Update (Write) data of a transparent EF"""
Harald Welte0d4e98a2021-04-07 00:14:40 +0200427 if opts.json_path:
428 (data_json, sw) = self._cmd.rs.read_binary_dec()
429 js_path_modify(data_json, opts.json_path, json.loads(opts.data))
430 else:
431 data_json = json.loads(opts.data)
Harald Welteb2edd142021-01-08 23:29:35 +0100432 (data, sw) = self._cmd.rs.update_binary_dec(data_json)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100433 if data:
Harald Welte1748b932021-04-06 21:12:25 +0200434 self._cmd.poutput_json(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100435
Harald Welteee3501f2021-04-02 13:00:18 +0200436 def __init__(self, fid:str, sfid:str=None, name:str=None, desc:str=None, parent:CardDF=None,
437 size={1,None}):
438 """
439 Args:
440 fid : File Identifier (4 hex digits)
441 sfid : Short File Identifier (2 hex digits, optional)
442 name : Brief name of the file, lik EF_ICCID
443 desc : Descriptoin of the file
444 parent : Parent CardFile object within filesystem hierarchy
445 size : tuple of (minimum_size, recommended_size)
446 """
Harald Welteb2edd142021-01-08 23:29:35 +0100447 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent)
448 self.size = size
449 self.shell_commands = [self.ShellCommands()]
450
Harald Welteee3501f2021-04-02 13:00:18 +0200451 def decode_bin(self, raw_bin_data:bytearray) -> dict:
452 """Decode raw (binary) data into abstract representation.
453
454 A derived class would typically provide a _decode_bin() or _decode_hex() method
455 for implementing this specifically for the given file. This function checks which
456 of the method exists, add calls them (with conversion, as needed).
457
458 Args:
459 raw_bin_data : binary encoded data
460 Returns:
461 abstract_data; dict representing the decoded data
462 """
Harald Welteb2edd142021-01-08 23:29:35 +0100463 method = getattr(self, '_decode_bin', None)
464 if callable(method):
465 return method(raw_bin_data)
466 method = getattr(self, '_decode_hex', None)
467 if callable(method):
468 return method(b2h(raw_bin_data))
469 return {'raw': raw_bin_data.hex()}
470
Harald Welteee3501f2021-04-02 13:00:18 +0200471 def decode_hex(self, raw_hex_data:str) -> dict:
472 """Decode raw (hex string) data into abstract representation.
473
474 A derived class would typically provide a _decode_bin() or _decode_hex() method
475 for implementing this specifically for the given file. This function checks which
476 of the method exists, add calls them (with conversion, as needed).
477
478 Args:
479 raw_hex_data : hex-encoded data
480 Returns:
481 abstract_data; dict representing the decoded data
482 """
Harald Welteb2edd142021-01-08 23:29:35 +0100483 method = getattr(self, '_decode_hex', None)
484 if callable(method):
485 return method(raw_hex_data)
486 raw_bin_data = h2b(raw_hex_data)
487 method = getattr(self, '_decode_bin', None)
488 if callable(method):
489 return method(raw_bin_data)
490 return {'raw': raw_bin_data.hex()}
491
Harald Welteee3501f2021-04-02 13:00:18 +0200492 def encode_bin(self, abstract_data:dict) -> bytearray:
493 """Encode abstract representation into raw (binary) data.
494
495 A derived class would typically provide an _encode_bin() or _encode_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 abstract_data : dict representing the decoded data
501 Returns:
502 binary encoded data
503 """
Harald Welteb2edd142021-01-08 23:29:35 +0100504 method = getattr(self, '_encode_bin', None)
505 if callable(method):
506 return method(abstract_data)
507 method = getattr(self, '_encode_hex', None)
508 if callable(method):
509 return h2b(method(abstract_data))
510 raise NotImplementedError
511
Harald Welteee3501f2021-04-02 13:00:18 +0200512 def encode_hex(self, abstract_data:dict) -> str:
513 """Encode abstract representation into raw (hex string) data.
514
515 A derived class would typically provide an _encode_bin() or _encode_hex() method
516 for implementing this specifically for the given file. This function checks which
517 of the method exists, add calls them (with conversion, as needed).
518
519 Args:
520 abstract_data : dict representing the decoded data
521 Returns:
522 hex string encoded data
523 """
Harald Welteb2edd142021-01-08 23:29:35 +0100524 method = getattr(self, '_encode_hex', None)
525 if callable(method):
526 return method(abstract_data)
527 method = getattr(self, '_encode_bin', None)
528 if callable(method):
529 raw_bin_data = method(abstract_data)
530 return b2h(raw_bin_data)
531 raise NotImplementedError
532
533
534class LinFixedEF(CardEF):
Harald Welteee3501f2021-04-02 13:00:18 +0200535 """Linear Fixed EF (Entry File) in the smart card filesystem.
536
537 Linear Fixed EFs are record oriented files. They consist of a number of fixed-size
538 records. The records can be individually read/updated."""
Harald Welteb2edd142021-01-08 23:29:35 +0100539
540 @with_default_category('Linear Fixed EF Commands')
541 class ShellCommands(CommandSet):
Harald Welteee3501f2021-04-02 13:00:18 +0200542 """Shell commands specific for Linear Fixed EFs."""
Harald Welteb2edd142021-01-08 23:29:35 +0100543 def __init__(self):
544 super().__init__()
545
546 read_rec_parser = argparse.ArgumentParser()
547 read_rec_parser.add_argument('record_nr', type=int, help='Number of record to be read')
Philipp Maier41555732021-02-25 16:52:08 +0100548 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 +0100549 @cmd2.with_argparser(read_rec_parser)
550 def do_read_record(self, opts):
Philipp Maier41555732021-02-25 16:52:08 +0100551 """Read one or multiple records from a record-oriented EF"""
552 for r in range(opts.count):
553 recnr = opts.record_nr + r
554 (data, sw) = self._cmd.rs.read_record(recnr)
555 if (len(data) > 0):
556 recstr = str(data)
557 else:
558 recstr = "(empty)"
559 self._cmd.poutput("%03d %s" % (recnr, recstr))
Harald Welteb2edd142021-01-08 23:29:35 +0100560
561 read_rec_dec_parser = argparse.ArgumentParser()
562 read_rec_dec_parser.add_argument('record_nr', type=int, help='Number of record to be read')
Harald Weltebcad86c2021-04-06 20:08:39 +0200563 read_rec_dec_parser.add_argument('--oneline', action='store_true',
564 help='No JSON pretty-printing, dump as a single line')
Harald Welteb2edd142021-01-08 23:29:35 +0100565 @cmd2.with_argparser(read_rec_dec_parser)
566 def do_read_record_decoded(self, opts):
567 """Read + decode a record from a record-oriented EF"""
568 (data, sw) = self._cmd.rs.read_record_dec(opts.record_nr)
Harald Welte1748b932021-04-06 21:12:25 +0200569 self._cmd.poutput_json(data, opts.oneline)
Harald Welteb2edd142021-01-08 23:29:35 +0100570
Harald Welte850b72a2021-04-07 09:33:03 +0200571 read_recs_parser = argparse.ArgumentParser()
572 @cmd2.with_argparser(read_recs_parser)
573 def do_read_records(self, opts):
574 """Read all records from a record-oriented EF"""
575 num_of_rec = self._cmd.rs.selected_file_fcp['file_descriptor']['num_of_rec']
576 for recnr in range(1, 1 + num_of_rec):
577 (data, sw) = self._cmd.rs.read_record(recnr)
578 if (len(data) > 0):
579 recstr = str(data)
580 else:
581 recstr = "(empty)"
582 self._cmd.poutput("%03d %s" % (recnr, recstr))
583
584 read_recs_dec_parser = argparse.ArgumentParser()
585 read_recs_dec_parser.add_argument('--oneline', action='store_true',
586 help='No JSON pretty-printing, dump as a single line')
587 @cmd2.with_argparser(read_recs_dec_parser)
588 def do_read_records_decoded(self, opts):
589 """Read + decode all records from a record-oriented EF"""
590 num_of_rec = self._cmd.rs.selected_file_fcp['file_descriptor']['num_of_rec']
591 # collect all results in list so they are rendered as JSON list when printing
592 data_list = []
593 for recnr in range(1, 1 + num_of_rec):
594 (data, sw) = self._cmd.rs.read_record_dec(recnr)
595 data_list.append(data)
596 self._cmd.poutput_json(data_list, opts.oneline)
597
Harald Welteb2edd142021-01-08 23:29:35 +0100598 upd_rec_parser = argparse.ArgumentParser()
599 upd_rec_parser.add_argument('record_nr', type=int, help='Number of record to be read')
600 upd_rec_parser.add_argument('data', help='Data bytes (hex format) to write')
601 @cmd2.with_argparser(upd_rec_parser)
602 def do_update_record(self, opts):
603 """Update (write) data to a record-oriented EF"""
604 (data, sw) = self._cmd.rs.update_record(opts.record_nr, opts.data)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100605 if data:
606 self._cmd.poutput(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100607
608 upd_rec_dec_parser = argparse.ArgumentParser()
609 upd_rec_dec_parser.add_argument('record_nr', type=int, help='Number of record to be read')
610 upd_rec_dec_parser.add_argument('data', help='Data bytes (hex format) to write')
Harald Welte0d4e98a2021-04-07 00:14:40 +0200611 upd_rec_dec_parser.add_argument('--json-path', type=str,
612 help='JSON path to modify specific element of record only')
Harald Welteb2edd142021-01-08 23:29:35 +0100613 @cmd2.with_argparser(upd_rec_dec_parser)
614 def do_update_record_decoded(self, opts):
615 """Encode + Update (write) data to a record-oriented EF"""
Harald Welte0d4e98a2021-04-07 00:14:40 +0200616 if opts.json_path:
617 (data_json, sw) = self._cmd.rs.read_record_dec(opts.record_nr)
618 js_path_modify(data_json, opts.json_path, json.loads(opts.data))
619 else:
620 data_json = json.loads(opts.data)
621 (data, sw) = self._cmd.rs.update_record_dec(opts.record_nr, data_json)
Philipp Maiere6bc4f92021-03-11 17:13:46 +0100622 if data:
623 self._cmd.poutput(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100624
Harald Welteee3501f2021-04-02 13:00:18 +0200625 def __init__(self, fid:str, sfid:str=None, name:str=None, desc:str=None,
626 parent:Optional[CardDF]=None, rec_len={1,None}):
627 """
628 Args:
629 fid : File Identifier (4 hex digits)
630 sfid : Short File Identifier (2 hex digits, optional)
631 name : Brief name of the file, lik EF_ICCID
632 desc : Descriptoin of the file
633 parent : Parent CardFile object within filesystem hierarchy
634 rec_len : tuple of (minimum_length, recommended_length)
635 """
Harald Welteb2edd142021-01-08 23:29:35 +0100636 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent)
637 self.rec_len = rec_len
638 self.shell_commands = [self.ShellCommands()]
639
Harald Welteee3501f2021-04-02 13:00:18 +0200640 def decode_record_hex(self, raw_hex_data:str) -> dict:
641 """Decode raw (hex string) data into abstract representation.
642
643 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
644 method for implementing this specifically for the given file. This function checks which
645 of the method exists, add calls them (with conversion, as needed).
646
647 Args:
648 raw_hex_data : hex-encoded data
649 Returns:
650 abstract_data; dict representing the decoded data
651 """
Harald Welteb2edd142021-01-08 23:29:35 +0100652 method = getattr(self, '_decode_record_hex', None)
653 if callable(method):
654 return method(raw_hex_data)
655 raw_bin_data = h2b(raw_hex_data)
656 method = getattr(self, '_decode_record_bin', None)
657 if callable(method):
658 return method(raw_bin_data)
659 return {'raw': raw_bin_data.hex()}
660
Harald Welteee3501f2021-04-02 13:00:18 +0200661 def decode_record_bin(self, raw_bin_data:bytearray) -> dict:
662 """Decode raw (binary) data into abstract representation.
663
664 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
665 method for implementing this specifically for the given file. This function checks which
666 of the method exists, add calls them (with conversion, as needed).
667
668 Args:
669 raw_bin_data : binary encoded data
670 Returns:
671 abstract_data; dict representing the decoded data
672 """
Harald Welteb2edd142021-01-08 23:29:35 +0100673 method = getattr(self, '_decode_record_bin', None)
674 if callable(method):
675 return method(raw_bin_data)
676 raw_hex_data = b2h(raw_bin_data)
677 method = getattr(self, '_decode_record_hex', None)
678 if callable(method):
679 return method(raw_hex_data)
680 return {'raw': raw_hex_data}
681
Harald Welteee3501f2021-04-02 13:00:18 +0200682 def encode_record_hex(self, abstract_data:dict) -> str:
683 """Encode abstract representation into raw (hex string) data.
684
685 A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
686 method for implementing this specifically for the given file. This function checks which
687 of the method exists, add calls them (with conversion, as needed).
688
689 Args:
690 abstract_data : dict representing the decoded data
691 Returns:
692 hex string encoded data
693 """
Harald Welteb2edd142021-01-08 23:29:35 +0100694 method = getattr(self, '_encode_record_hex', None)
695 if callable(method):
696 return method(abstract_data)
697 method = getattr(self, '_encode_record_bin', None)
698 if callable(method):
699 raw_bin_data = method(abstract_data)
Harald Welte1e456572021-04-02 17:16:30 +0200700 return b2h(raw_bin_data)
Harald Welteb2edd142021-01-08 23:29:35 +0100701 raise NotImplementedError
702
Harald Welteee3501f2021-04-02 13:00:18 +0200703 def encode_record_bin(self, abstract_data:dict) -> bytearray:
704 """Encode abstract representation into raw (binary) data.
705
706 A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
707 method for implementing this specifically for the given file. This function checks which
708 of the method exists, add calls them (with conversion, as needed).
709
710 Args:
711 abstract_data : dict representing the decoded data
712 Returns:
713 binary encoded data
714 """
Harald Welteb2edd142021-01-08 23:29:35 +0100715 method = getattr(self, '_encode_record_bin', None)
716 if callable(method):
717 return method(abstract_data)
718 method = getattr(self, '_encode_record_hex', None)
719 if callable(method):
Harald Welteee3501f2021-04-02 13:00:18 +0200720 return h2b(method(abstract_data))
Harald Welteb2edd142021-01-08 23:29:35 +0100721 raise NotImplementedError
722
723class CyclicEF(LinFixedEF):
724 """Cyclic EF (Entry File) in the smart card filesystem"""
725 # we don't really have any special support for those; just recycling LinFixedEF here
Harald Welteee3501f2021-04-02 13:00:18 +0200726 def __init__(self, fid:str, sfid:str=None, name:str=None, desc:str=None, parent:CardDF=None,
727 rec_len={1,None}):
Harald Welteb2edd142021-01-08 23:29:35 +0100728 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, rec_len=rec_len)
729
730class TransRecEF(TransparentEF):
731 """Transparent EF (Entry File) containing fixed-size records.
Harald Welteee3501f2021-04-02 13:00:18 +0200732
Harald Welteb2edd142021-01-08 23:29:35 +0100733 These are the real odd-balls and mostly look like mistakes in the specification:
734 Specified as 'transparent' EF, but actually containing several fixed-length records
735 inside.
736 We add a special class for those, so the user only has to provide encoder/decoder functions
737 for a record, while this class takes care of split / merge of records.
738 """
Harald Welte1e456572021-04-02 17:16:30 +0200739 def __init__(self, fid:str, rec_len:int, sfid:str=None, name:str=None, desc:str=None,
740 parent:Optional[CardDF]=None, size={1,None}):
Harald Welteee3501f2021-04-02 13:00:18 +0200741 """
742 Args:
743 fid : File Identifier (4 hex digits)
744 sfid : Short File Identifier (2 hex digits, optional)
745 name : Brief name of the file, lik EF_ICCID
746 desc : Descriptoin of the file
747 parent : Parent CardFile object within filesystem hierarchy
748 rec_len : Length of the fixed-length records within transparent EF
749 size : tuple of (minimum_size, recommended_size)
750 """
Harald Welteb2edd142021-01-08 23:29:35 +0100751 super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, size=size)
752 self.rec_len = rec_len
753
Harald Welteee3501f2021-04-02 13:00:18 +0200754 def decode_record_hex(self, raw_hex_data:str) -> dict:
755 """Decode raw (hex string) data into abstract representation.
756
757 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
758 method for implementing this specifically for the given file. This function checks which
759 of the method exists, add calls them (with conversion, as needed).
760
761 Args:
762 raw_hex_data : hex-encoded data
763 Returns:
764 abstract_data; dict representing the decoded data
765 """
Harald Welteb2edd142021-01-08 23:29:35 +0100766 method = getattr(self, '_decode_record_hex', None)
767 if callable(method):
768 return method(raw_hex_data)
769 method = getattr(self, '_decode_record_bin', None)
770 if callable(method):
771 raw_bin_data = h2b(raw_hex_data)
772 return method(raw_bin_data)
773 return {'raw': raw_hex_data}
774
Harald Welteee3501f2021-04-02 13:00:18 +0200775 def decode_record_bin(self, raw_bin_data:bytearray) -> dict:
776 """Decode raw (binary) data into abstract representation.
777
778 A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
779 method for implementing this specifically for the given file. This function checks which
780 of the method exists, add calls them (with conversion, as needed).
781
782 Args:
783 raw_bin_data : binary encoded data
784 Returns:
785 abstract_data; dict representing the decoded data
786 """
Harald Welteb2edd142021-01-08 23:29:35 +0100787 method = getattr(self, '_decode_record_bin', None)
788 if callable(method):
789 return method(raw_bin_data)
790 raw_hex_data = b2h(raw_bin_data)
791 method = getattr(self, '_decode_record_hex', None)
792 if callable(method):
793 return method(raw_hex_data)
794 return {'raw': raw_hex_data}
795
Harald Welteee3501f2021-04-02 13:00:18 +0200796 def encode_record_hex(self, abstract_data:dict) -> str:
797 """Encode abstract representation into raw (hex string) data.
798
799 A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
800 method for implementing this specifically for the given file. This function checks which
801 of the method exists, add calls them (with conversion, as needed).
802
803 Args:
804 abstract_data : dict representing the decoded data
805 Returns:
806 hex string encoded data
807 """
Harald Welteb2edd142021-01-08 23:29:35 +0100808 method = getattr(self, '_encode_record_hex', None)
809 if callable(method):
810 return method(abstract_data)
811 method = getattr(self, '_encode_record_bin', None)
812 if callable(method):
Harald Welte1e456572021-04-02 17:16:30 +0200813 return b2h(method(abstract_data))
Harald Welteb2edd142021-01-08 23:29:35 +0100814 raise NotImplementedError
815
Harald Welteee3501f2021-04-02 13:00:18 +0200816 def encode_record_bin(self, abstract_data:dict) -> bytearray:
817 """Encode abstract representation into raw (binary) data.
818
819 A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
820 method for implementing this specifically for the given file. This function checks which
821 of the method exists, add calls them (with conversion, as needed).
822
823 Args:
824 abstract_data : dict representing the decoded data
825 Returns:
826 binary encoded data
827 """
Harald Welteb2edd142021-01-08 23:29:35 +0100828 method = getattr(self, '_encode_record_bin', None)
829 if callable(method):
830 return method(abstract_data)
831 method = getattr(self, '_encode_record_hex', None)
832 if callable(method):
833 return h2b(method(abstract_data))
834 raise NotImplementedError
835
Harald Welteee3501f2021-04-02 13:00:18 +0200836 def _decode_bin(self, raw_bin_data:bytearray):
Harald Welteb2edd142021-01-08 23:29:35 +0100837 chunks = [raw_bin_data[i:i+self.rec_len] for i in range(0, len(raw_bin_data), self.rec_len)]
838 return [self.decode_record_bin(x) for x in chunks]
839
Harald Welteee3501f2021-04-02 13:00:18 +0200840 def _encode_bin(self, abstract_data) -> bytes:
Harald Welteb2edd142021-01-08 23:29:35 +0100841 chunks = [self.encode_record_bin(x) for x in abstract_data]
842 # FIXME: pad to file size
843 return b''.join(chunks)
844
845
846
847
848
849class RuntimeState(object):
850 """Represent the runtime state of a session with a card."""
Harald Welteee3501f2021-04-02 13:00:18 +0200851 def __init__(self, card, profile:'CardProfile'):
852 """
853 Args:
854 card : pysim.cards.Card instance
855 profile : CardProfile instance
856 """
Harald Welteb2edd142021-01-08 23:29:35 +0100857 self.mf = CardMF()
858 self.card = card
Harald Welte5ce35242021-04-02 20:27:05 +0200859 self.selected_file:CardDF = self.mf
Harald Welteb2edd142021-01-08 23:29:35 +0100860 self.profile = profile
Harald Welte5ce35242021-04-02 20:27:05 +0200861 # add application ADFs + MF-files from profile
Philipp Maier1e896f32021-03-10 17:02:53 +0100862 apps = self._match_applications()
863 for a in apps:
Harald Welte5ce35242021-04-02 20:27:05 +0200864 if a.adf:
865 self.mf.add_application_df(a.adf)
Harald Welteb2edd142021-01-08 23:29:35 +0100866 for f in self.profile.files_in_mf:
867 self.mf.add_file(f)
Philipp Maier38c74f62021-03-17 17:19:52 +0100868 self.conserve_write = True
Harald Welteb2edd142021-01-08 23:29:35 +0100869
Philipp Maier1e896f32021-03-10 17:02:53 +0100870 def _match_applications(self):
871 """match the applications from the profile with applications on the card"""
872 apps_profile = self.profile.applications
873 aids_card = self.card.read_aids()
874 apps_taken = []
875 if aids_card:
876 aids_taken = []
877 print("AIDs on card:")
878 for a in aids_card:
879 for f in apps_profile:
880 if f.aid in a:
881 print(" %s: %s" % (f.name, a))
882 aids_taken.append(a)
883 apps_taken.append(f)
884 aids_unknown = set(aids_card) - set(aids_taken)
885 for a in aids_unknown:
886 print(" unknown: %s" % a)
887 else:
888 print("error: could not determine card applications")
889 return apps_taken
890
Harald Welteee3501f2021-04-02 13:00:18 +0200891 def get_cwd(self) -> CardDF:
892 """Obtain the current working directory.
893
894 Returns:
895 CardDF instance
896 """
Harald Welteb2edd142021-01-08 23:29:35 +0100897 if isinstance(self.selected_file, CardDF):
898 return self.selected_file
899 else:
900 return self.selected_file.parent
901
Harald Welte5ce35242021-04-02 20:27:05 +0200902 def get_application_df(self) -> Optional[CardADF]:
903 """Obtain the currently selected application DF (if any).
Harald Welteee3501f2021-04-02 13:00:18 +0200904
905 Returns:
906 CardADF() instance or None"""
Harald Welteb2edd142021-01-08 23:29:35 +0100907 # iterate upwards from selected file; check if any is an ADF
908 node = self.selected_file
909 while node.parent != node:
910 if isinstance(node, CardADF):
911 return node
912 node = node.parent
913 return None
914
Harald Welteee3501f2021-04-02 13:00:18 +0200915 def interpret_sw(self, sw:str):
916 """Interpret a given status word relative to the currently selected application
917 or the underlying card profile.
918
919 Args:
920 sw : Status word as string of 4 hexd digits
921
922 Returns:
923 Tuple of two strings
924 """
Harald Welte86fbd392021-04-02 22:13:09 +0200925 res = None
Harald Welte5ce35242021-04-02 20:27:05 +0200926 adf = self.get_application_df()
927 if adf:
928 app = adf.application
Harald Welteb2edd142021-01-08 23:29:35 +0100929 # The application either comes with its own interpret_sw
930 # method or we will use the interpret_sw method from the
931 # card profile.
Harald Welte5ce35242021-04-02 20:27:05 +0200932 if app and hasattr(app, "interpret_sw"):
Harald Welte86fbd392021-04-02 22:13:09 +0200933 res = app.interpret_sw(sw)
934 return res or self.profile.interpret_sw(sw)
Harald Welteb2edd142021-01-08 23:29:35 +0100935
Harald Welteee3501f2021-04-02 13:00:18 +0200936 def probe_file(self, fid:str, cmd_app=None):
937 """Blindly try to select a file and automatically add a matching file
938 object if the file actually exists."""
Philipp Maier63f572d2021-03-09 22:42:47 +0100939 if not is_hex(fid, 4, 4):
940 raise ValueError("Cannot select unknown file by name %s, only hexadecimal 4 digit FID is allowed" % fid)
941
942 try:
943 (data, sw) = self.card._scc.select_file(fid)
944 except SwMatchError as swm:
945 k = self.interpret_sw(swm.sw_actual)
946 if not k:
947 raise(swm)
948 raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1]))
949
950 select_resp = self.selected_file.decode_select_response(data)
951 if (select_resp['file_descriptor']['file_type'] == 'df'):
952 f = CardDF(fid=fid, sfid=None, name="DF." + str(fid).upper(), desc="dedicated file, manually added at runtime")
953 else:
954 if (select_resp['file_descriptor']['structure'] == 'transparent'):
955 f = TransparentEF(fid=fid, sfid=None, name="EF." + str(fid).upper(), desc="elementry file, manually added at runtime")
956 else:
957 f = LinFixedEF(fid=fid, sfid=None, name="EF." + str(fid).upper(), desc="elementry file, manually added at runtime")
958
959 self.selected_file.add_files([f])
960 self.selected_file = f
961 return select_resp
962
Harald Welteee3501f2021-04-02 13:00:18 +0200963 def select(self, name:str, cmd_app=None):
964 """Select a file (EF, DF, ADF, MF, ...).
965
966 Args:
967 name : Name of file to select
968 cmd_app : Command Application State (for unregistering old file commands)
969 """
Harald Welteb2edd142021-01-08 23:29:35 +0100970 sels = self.selected_file.get_selectables()
Philipp Maier7744b6e2021-03-11 14:29:37 +0100971 if is_hex(name):
972 name = name.lower()
Philipp Maier63f572d2021-03-09 22:42:47 +0100973
974 # unregister commands of old file
975 if cmd_app and self.selected_file.shell_commands:
976 for c in self.selected_file.shell_commands:
977 cmd_app.unregister_command_set(c)
978
Harald Welteb2edd142021-01-08 23:29:35 +0100979 if name in sels:
980 f = sels[name]
Harald Welteb2edd142021-01-08 23:29:35 +0100981 try:
982 if isinstance(f, CardADF):
Philipp Maiercba6dbc2021-03-11 13:03:18 +0100983 (data, sw) = self.card.select_adf_by_aid(f.aid)
Harald Welteb2edd142021-01-08 23:29:35 +0100984 else:
985 (data, sw) = self.card._scc.select_file(f.fid)
986 self.selected_file = f
987 except SwMatchError as swm:
988 k = self.interpret_sw(swm.sw_actual)
989 if not k:
990 raise(swm)
991 raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1]))
Philipp Maier63f572d2021-03-09 22:42:47 +0100992 select_resp = f.decode_select_response(data)
Harald Welteb2edd142021-01-08 23:29:35 +0100993 else:
Philipp Maier63f572d2021-03-09 22:42:47 +0100994 select_resp = self.probe_file(name, cmd_app)
Harald Welte850b72a2021-04-07 09:33:03 +0200995 # store the decoded FCP for later reference
996 self.selected_file_fcp = select_resp
Philipp Maier63f572d2021-03-09 22:42:47 +0100997
998 # register commands of new file
999 if cmd_app and self.selected_file.shell_commands:
1000 for c in self.selected_file.shell_commands:
1001 cmd_app.register_command_set(c)
1002
1003 return select_resp
Harald Welteb2edd142021-01-08 23:29:35 +01001004
Harald Welteee3501f2021-04-02 13:00:18 +02001005 def read_binary(self, length:int=None, offset:int=0):
1006 """Read [part of] a transparent EF binary data.
1007
1008 Args:
1009 length : Amount of data to read (None: as much as possible)
1010 offset : Offset into the file from which to read 'length' bytes
1011 Returns:
1012 binary data read from the file
1013 """
Harald Welteb2edd142021-01-08 23:29:35 +01001014 if not isinstance(self.selected_file, TransparentEF):
1015 raise TypeError("Only works with TransparentEF")
1016 return self.card._scc.read_binary(self.selected_file.fid, length, offset)
1017
Harald Welte2d4a64b2021-04-03 09:01:24 +02001018 def read_binary_dec(self) -> Tuple[dict, str]:
Harald Welteee3501f2021-04-02 13:00:18 +02001019 """Read [part of] a transparent EF binary data and decode it.
1020
1021 Args:
1022 length : Amount of data to read (None: as much as possible)
1023 offset : Offset into the file from which to read 'length' bytes
1024 Returns:
1025 abstract decode data read from the file
1026 """
Harald Welteb2edd142021-01-08 23:29:35 +01001027 (data, sw) = self.read_binary()
1028 dec_data = self.selected_file.decode_hex(data)
Harald Welteb2edd142021-01-08 23:29:35 +01001029 return (dec_data, sw)
1030
Harald Welteee3501f2021-04-02 13:00:18 +02001031 def update_binary(self, data_hex:str, offset:int=0):
1032 """Update transparent EF binary data.
1033
1034 Args:
1035 data_hex : hex string of data to be written
1036 offset : Offset into the file from which to write 'data_hex'
1037 """
Harald Welteb2edd142021-01-08 23:29:35 +01001038 if not isinstance(self.selected_file, TransparentEF):
1039 raise TypeError("Only works with TransparentEF")
Philipp Maier38c74f62021-03-17 17:19:52 +01001040 return self.card._scc.update_binary(self.selected_file.fid, data_hex, offset, conserve=self.conserve_write)
Harald Welteb2edd142021-01-08 23:29:35 +01001041
Harald Welteee3501f2021-04-02 13:00:18 +02001042 def update_binary_dec(self, data:dict):
1043 """Update transparent EF from abstract data. Encodes the data to binary and
1044 then updates the EF with it.
1045
1046 Args:
1047 data : abstract data which is to be encoded and written
1048 """
Harald Welteb2edd142021-01-08 23:29:35 +01001049 data_hex = self.selected_file.encode_hex(data)
Harald Welteb2edd142021-01-08 23:29:35 +01001050 return self.update_binary(data_hex)
1051
Harald Welteee3501f2021-04-02 13:00:18 +02001052 def read_record(self, rec_nr:int=0):
1053 """Read a record as binary data.
1054
1055 Args:
1056 rec_nr : Record number to read
1057 Returns:
1058 hex string of binary data contained in record
1059 """
Harald Welteb2edd142021-01-08 23:29:35 +01001060 if not isinstance(self.selected_file, LinFixedEF):
1061 raise TypeError("Only works with Linear Fixed EF")
1062 # returns a string of hex nibbles
1063 return self.card._scc.read_record(self.selected_file.fid, rec_nr)
1064
Harald Welteee3501f2021-04-02 13:00:18 +02001065 def read_record_dec(self, rec_nr:int=0) -> Tuple[dict, str]:
1066 """Read a record and decode it to abstract data.
1067
1068 Args:
1069 rec_nr : Record number to read
1070 Returns:
1071 abstract data contained in record
1072 """
Harald Welteb2edd142021-01-08 23:29:35 +01001073 (data, sw) = self.read_record(rec_nr)
1074 return (self.selected_file.decode_record_hex(data), sw)
1075
Harald Welteee3501f2021-04-02 13:00:18 +02001076 def update_record(self, rec_nr:int, data_hex:str):
1077 """Update a record with given binary data
1078
1079 Args:
1080 rec_nr : Record number to read
1081 data_hex : Hex string binary data to be written
1082 """
Harald Welteb2edd142021-01-08 23:29:35 +01001083 if not isinstance(self.selected_file, LinFixedEF):
1084 raise TypeError("Only works with Linear Fixed EF")
Philipp Maier38c74f62021-03-17 17:19:52 +01001085 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 +01001086
Harald Welteee3501f2021-04-02 13:00:18 +02001087 def update_record_dec(self, rec_nr:int, data:dict):
1088 """Update a record with given abstract data. Will encode abstract to binary data
1089 and then write it to the given record on the card.
1090
1091 Args:
1092 rec_nr : Record number to read
1093 data_hex : Abstract data to be written
1094 """
Harald Welte1e456572021-04-02 17:16:30 +02001095 data_hex = self.selected_file.encode_record_hex(data)
1096 return self.update_record(rec_nr, data_hex)
Harald Welteb2edd142021-01-08 23:29:35 +01001097
1098
1099
1100class FileData(object):
1101 """Represent the runtime, on-card data."""
1102 def __init__(self, fdesc):
1103 self.desc = fdesc
1104 self.fcp = None
1105
1106
Harald Welteee3501f2021-04-02 13:00:18 +02001107def interpret_sw(sw_data:dict, sw:str):
1108 """Interpret a given status word.
1109
1110 Args:
1111 sw_data : Hierarchical dict of status word matches
1112 sw : status word to match (string of 4 hex digits)
1113 Returns:
1114 tuple of two strings (class_string, description)
1115 """
Harald Welteb2edd142021-01-08 23:29:35 +01001116 for class_str, swdict in sw_data.items():
1117 # first try direct match
1118 if sw in swdict:
1119 return (class_str, swdict[sw])
1120 # next try wildcard matches
1121 for pattern, descr in swdict.items():
1122 if sw_match(sw, pattern):
1123 return (class_str, descr)
1124 return None
1125
1126class CardApplication(object):
1127 """A card application is represented by an ADF (with contained hierarchy) and optionally
1128 some SW definitions."""
Harald Welte5ce35242021-04-02 20:27:05 +02001129 def __init__(self, name, adf:Optional[CardADF]=None, aid:str=None, sw:dict=None):
Harald Welteee3501f2021-04-02 13:00:18 +02001130 """
1131 Args:
1132 adf : ADF name
1133 sw : Dict of status word conversions
1134 """
Harald Welteb2edd142021-01-08 23:29:35 +01001135 self.name = name
1136 self.adf = adf
Vadim Yanitskiy98f872b2021-03-27 01:25:46 +01001137 self.sw = sw or dict()
Harald Welte5ce35242021-04-02 20:27:05 +02001138 # back-reference from ADF to Applicaiton
1139 if self.adf:
1140 self.aid = aid or self.adf.aid
1141 self.adf.application = self
1142 else:
1143 self.aid = aid
Harald Welteb2edd142021-01-08 23:29:35 +01001144
1145 def __str__(self):
1146 return "APP(%s)" % (self.name)
1147
1148 def interpret_sw(self, sw):
Harald Welteee3501f2021-04-02 13:00:18 +02001149 """Interpret a given status word within the application.
1150
1151 Args:
1152 sw : Status word as string of 4 hexd digits
1153
1154 Returns:
1155 Tuple of two strings
1156 """
Harald Welteb2edd142021-01-08 23:29:35 +01001157 return interpret_sw(self.sw, sw)
1158
1159class CardProfile(object):
1160 """A Card Profile describes a card, it's filessystem hierarchy, an [initial] list of
1161 applications as well as profile-specific SW and shell commands. Every card has
1162 one card profile, but there may be multiple applications within that profile."""
Vadim Yanitskiy98f872b2021-03-27 01:25:46 +01001163 def __init__(self, name, **kw):
Harald Welteee3501f2021-04-02 13:00:18 +02001164 """
1165 Args:
1166 desc (str) : Description
1167 files_in_mf : List of CardEF instances present in MF
1168 applications : List of CardApplications present on card
1169 sw : List of status word definitions
1170 shell_cmdsets : List of cmd2 shell command sets of profile-specific commands
1171 """
Harald Welteb2edd142021-01-08 23:29:35 +01001172 self.name = name
Vadim Yanitskiy98f872b2021-03-27 01:25:46 +01001173 self.desc = kw.get("desc", None)
1174 self.files_in_mf = kw.get("files_in_mf", [])
1175 self.sw = kw.get("sw", [])
1176 self.applications = kw.get("applications", [])
1177 self.shell_cmdsets = kw.get("shell_cmdsets", [])
Harald Welteb2edd142021-01-08 23:29:35 +01001178
1179 def __str__(self):
1180 return self.name
1181
Harald Welteee3501f2021-04-02 13:00:18 +02001182 def add_application(self, app:CardApplication):
1183 """Add an application to a card profile.
1184
1185 Args:
1186 app : CardApplication instance to be added to profile
1187 """
Philipp Maiereb72fa42021-03-26 21:29:57 +01001188 self.applications.append(app)
Harald Welteb2edd142021-01-08 23:29:35 +01001189
Harald Welteee3501f2021-04-02 13:00:18 +02001190 def interpret_sw(self, sw:str):
1191 """Interpret a given status word within the profile.
1192
1193 Args:
1194 sw : Status word as string of 4 hexd digits
1195
1196 Returns:
1197 Tuple of two strings
1198 """
Harald Welteb2edd142021-01-08 23:29:35 +01001199 return interpret_sw(self.sw, sw)