mirror of https://gerrit.osmocom.org/pysim
BER-TLV EF support (command, filesystem, shell)
This adds support for a new EF file type: BER-TLV files. They are different from transparent and linear fixed EFs in that they neither operate on a byte stream nor fixed-sized records, but on BER-TLV encoded objects. One can specify a tag value, and the card will return the entire TLV for that tag. As indicated in the spec, the magic tag value 0x5C (92) will return a list of tags existing in the file. Change-Id: Ibfcce757dcd477fd0d6857f64fbb4346d6d62e63
This commit is contained in:
parent
fc4833ec20
commit
917d98c1a5
|
@ -334,6 +334,43 @@ to the SIM card.
|
|||
This allows for easy interactive modification of file contents.
|
||||
|
||||
|
||||
|
||||
BER-TLV EF commands
|
||||
-------------------
|
||||
|
||||
BER-TLV EFs are files that contain BER-TLV structured data. Every file can contain any number
|
||||
of variable-length IEs (DOs). The tag within a BER-TLV EF must be unique within the file.
|
||||
|
||||
The commands below become enabled only when your currently selected file is of *BER-TLV EF* type.
|
||||
|
||||
retrieve_tags
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Retrieve a list of all tags present in the currently selected file.
|
||||
|
||||
|
||||
retrieve_data
|
||||
~~~~~~~~~~~~~
|
||||
.. argparse::
|
||||
:module: pySim.filesystem
|
||||
:func: BerTlvEF.ShellCommands.retrieve_data_parser
|
||||
|
||||
|
||||
set_data
|
||||
~~~~~~~~
|
||||
.. argparse::
|
||||
:module: pySim.filesystem
|
||||
:func: BerTlvEF.ShellCommands.set_data_parser
|
||||
|
||||
|
||||
del_data
|
||||
~~~~~~~~
|
||||
.. argparse::
|
||||
:module: pySim.filesystem
|
||||
:func: BerTlvEF.ShellCommands.del_data_parser
|
||||
|
||||
|
||||
|
||||
USIM commands
|
||||
-------------
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ from pySim.exceptions import *
|
|||
from pySim.commands import SimCardCommands
|
||||
from pySim.transport import init_reader, ApduTracer, argparse_add_reader_args
|
||||
from pySim.cards import card_detect, Card
|
||||
from pySim.utils import h2b, swap_nibbles, rpad, h2s, JsonEncoder
|
||||
from pySim.utils import h2b, swap_nibbles, rpad, b2h, h2s, JsonEncoder, bertlv_parse_one
|
||||
from pySim.utils import dec_st, sanitize_pin_adm, tabulate_str_list, is_hex, boxed_heading_str
|
||||
from pySim.card_handler import card_handler
|
||||
|
||||
|
@ -256,11 +256,19 @@ class PySimCommands(CommandSet):
|
|||
if structure == 'transparent':
|
||||
result = self._cmd.rs.read_binary()
|
||||
self._cmd.poutput("update_binary " + str(result[0]))
|
||||
if structure == 'cyclic' or structure == 'linear_fixed':
|
||||
elif structure == 'cyclic' or structure == 'linear_fixed':
|
||||
num_of_rec = fd['num_of_rec']
|
||||
for r in range(1, num_of_rec + 1):
|
||||
result = self._cmd.rs.read_record(r)
|
||||
self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
|
||||
elif structure == 'ber_tlv':
|
||||
tags = self._cmd.rs.retrieve_tags()
|
||||
for t in tags:
|
||||
result = self._cmd.rs.retrieve_data(t)
|
||||
(tag, l, val) = bertlv_parse_one(h2b(result[0]))
|
||||
self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val)))
|
||||
else:
|
||||
raise RuntimeError('Unsupported structure "%s" of file "%s"' % (structure, filename))
|
||||
except Exception as e:
|
||||
bad_file_str = '/'.join(df_path_list) + "/" + str(filename) + ", " + str(e)
|
||||
self._cmd.poutput("# bad file: %s" % bad_file_str)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
#
|
||||
# Copyright (C) 2009-2010 Sylvain Munaut <tnt@246tNt.com>
|
||||
# Copyright (C) 2010 Harald Welte <laforge@gnumonks.org>
|
||||
# Copyright (C) 2010-2021 Harald Welte <laforge@gnumonks.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -23,7 +23,7 @@
|
|||
|
||||
from construct import *
|
||||
from pySim.construct import LV
|
||||
from pySim.utils import rpad, b2h, sw_match
|
||||
from pySim.utils import rpad, b2h, h2b, sw_match, bertlv_encode_len
|
||||
from pySim.exceptions import SwMatchError
|
||||
|
||||
class SimCardCommands(object):
|
||||
|
@ -269,6 +269,76 @@ class SimCardCommands(object):
|
|||
r = self.select_path(ef)
|
||||
return self.__len(r)
|
||||
|
||||
# TS 102 221 Section 11.3.1 low-level helper
|
||||
def _retrieve_data(self, tag:int, first:bool=True):
|
||||
if first:
|
||||
pdu = '80cb008001%02x' % (tag)
|
||||
else:
|
||||
pdu = '80cb000000'
|
||||
return self._tp.send_apdu_checksw(pdu)
|
||||
|
||||
# TS 102 221 Section 11.3.1
|
||||
def retrieve_data(self, ef, tag:int):
|
||||
"""Execute RETRIEVE DATA.
|
||||
|
||||
Args
|
||||
ef : string or list of strings indicating name or path of transparent EF
|
||||
tag : BER-TLV Tag of value to be retrieved
|
||||
"""
|
||||
r = self.select_path(ef)
|
||||
if len(r[-1]) == 0:
|
||||
return (None, None)
|
||||
total_data = ''
|
||||
# retrieve first block
|
||||
data, sw = self._retrieve_data(tag, first=True)
|
||||
total_data += data
|
||||
while sw == '62f1' or sw == '62f2':
|
||||
data, sw = self._retrieve_data(tag, first=False)
|
||||
total_data += data
|
||||
return total_data, sw
|
||||
|
||||
# TS 102 221 Section 11.3.2 low-level helper
|
||||
def _set_data(self, data:str, first:bool=True):
|
||||
if first:
|
||||
p1 = 0x80
|
||||
else:
|
||||
p1 = 0x00
|
||||
if isinstance(data, bytes) or isinstance(data, bytearray):
|
||||
data = b2h(data)
|
||||
pdu = '80db00%02x%02x%s' % (p1, len(data)//2, data)
|
||||
return self._tp.send_apdu_checksw(pdu)
|
||||
|
||||
def set_data(self, ef, tag:int, value:str, verify:bool=False, conserve:bool=False):
|
||||
"""Execute SET DATA.
|
||||
|
||||
Args
|
||||
ef : string or list of strings indicating name or path of transparent EF
|
||||
tag : BER-TLV Tag of value to be stored
|
||||
value : BER-TLV value to be stored
|
||||
"""
|
||||
r = self.select_path(ef)
|
||||
if len(r[-1]) == 0:
|
||||
return (None, None)
|
||||
|
||||
# in case of deleting the data, we only have 'tag' but no 'value'
|
||||
if not value:
|
||||
return self._set_data('%02x' % tag, first=True)
|
||||
|
||||
# FIXME: proper BER-TLV encode
|
||||
tl = '%02x%s' % (tag, b2h(bertlv_encode_len(len(value)//2)))
|
||||
tlv = tl + value
|
||||
tlv_bin = h2b(tlv)
|
||||
|
||||
first = True
|
||||
total_len = len(tlv_bin)
|
||||
remaining = tlv_bin
|
||||
while len(remaining) > 0:
|
||||
fragment = remaining[:255]
|
||||
rdata, sw = self._set_data(fragment, first=first)
|
||||
first = False
|
||||
remaining = remaining[255:]
|
||||
return rdata, sw
|
||||
|
||||
def run_gsm(self, rand:str):
|
||||
"""Execute RUN GSM ALGORITHM."""
|
||||
if len(rand) != 32:
|
||||
|
|
|
@ -34,7 +34,7 @@ import argparse
|
|||
|
||||
from typing import cast, Optional, Iterable, List, Any, Dict, Tuple
|
||||
|
||||
from pySim.utils import sw_match, h2b, b2h, is_hex
|
||||
from pySim.utils import sw_match, h2b, b2h, is_hex, auto_int, bertlv_parse_one
|
||||
from pySim.construct import filter_dict
|
||||
from pySim.exceptions import *
|
||||
from pySim.jsonpath import js_path_find, js_path_modify
|
||||
|
@ -914,7 +914,7 @@ class TransRecEF(TransparentEF):
|
|||
return b''.join(chunks)
|
||||
|
||||
|
||||
class BerTlvEF(TransparentEF):
|
||||
class BerTlvEF(CardEF):
|
||||
"""BER-TLV EF (Entry File) in the smart card filesystem.
|
||||
A BER-TLV EF is a binary file with a BER (Basic Encoding Rules) TLV structure
|
||||
|
||||
|
@ -922,6 +922,61 @@ class BerTlvEF(TransparentEF):
|
|||
around TransparentEF as a place-holder, so we can already define EFs of BER-TLV
|
||||
type without fully supporting them."""
|
||||
|
||||
@with_default_category('BER-TLV EF Commands')
|
||||
class ShellCommands(CommandSet):
|
||||
"""Shell commands specific for BER-TLV EFs."""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
retrieve_data_parser = argparse.ArgumentParser()
|
||||
retrieve_data_parser.add_argument('tag', type=auto_int, help='BER-TLV Tag of value to retrieve')
|
||||
@cmd2.with_argparser(retrieve_data_parser)
|
||||
def do_retrieve_data(self, opts):
|
||||
"""Retrieve (Read) data from a BER-TLV EF"""
|
||||
(data, sw) = self._cmd.rs.retrieve_data(opts.tag)
|
||||
self._cmd.poutput(data)
|
||||
|
||||
def do_retrieve_tags(self, opts):
|
||||
"""List tags available in a given BER-TLV EF"""
|
||||
tags = self._cmd.rs.retrieve_tags()
|
||||
self._cmd.poutput(tags)
|
||||
|
||||
set_data_parser = argparse.ArgumentParser()
|
||||
set_data_parser.add_argument('tag', type=auto_int, help='BER-TLV Tag of value to set')
|
||||
set_data_parser.add_argument('data', help='Data bytes (hex format) to write')
|
||||
@cmd2.with_argparser(set_data_parser)
|
||||
def do_set_data(self, opts):
|
||||
"""Set (Write) data for a given tag in a BER-TLV EF"""
|
||||
(data, sw) = self._cmd.rs.set_data(opts.tag, opts.data)
|
||||
if data:
|
||||
self._cmd.poutput(data)
|
||||
|
||||
del_data_parser = argparse.ArgumentParser()
|
||||
del_data_parser.add_argument('tag', type=auto_int, help='BER-TLV Tag of value to set')
|
||||
@cmd2.with_argparser(del_data_parser)
|
||||
def do_delete_data(self, opts):
|
||||
"""Delete data for a given tag in a BER-TLV EF"""
|
||||
(data, sw) = self._cmd.rs.set_data(opts.tag, None)
|
||||
if data:
|
||||
self._cmd.poutput(data)
|
||||
|
||||
|
||||
def __init__(self, fid:str, sfid:str=None, name:str=None, desc:str=None, parent:CardDF=None,
|
||||
size={1,None}):
|
||||
"""
|
||||
Args:
|
||||
fid : File Identifier (4 hex digits)
|
||||
sfid : Short File Identifier (2 hex digits, optional)
|
||||
name : Brief name of the file, lik EF_ICCID
|
||||
desc : Description of the file
|
||||
parent : Parent CardFile object within filesystem hierarchy
|
||||
size : tuple of (minimum_size, recommended_size)
|
||||
"""
|
||||
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent)
|
||||
self._construct = None
|
||||
self.size = size
|
||||
self.shell_commands = [self.ShellCommands()]
|
||||
|
||||
|
||||
class RuntimeState(object):
|
||||
"""Represent the runtime state of a session with a card."""
|
||||
|
@ -1172,6 +1227,43 @@ class RuntimeState(object):
|
|||
data_hex = self.selected_file.encode_record_hex(data)
|
||||
return self.update_record(rec_nr, data_hex)
|
||||
|
||||
def retrieve_data(self, tag:int=0):
|
||||
"""Read a DO/TLV as binary data.
|
||||
|
||||
Args:
|
||||
tag : Tag of TLV/DO to read
|
||||
Returns:
|
||||
hex string of full BER-TLV DO including Tag and Length
|
||||
"""
|
||||
if not isinstance(self.selected_file, BerTlvEF):
|
||||
raise TypeError("Only works with BER-TLV EF")
|
||||
# returns a string of hex nibbles
|
||||
return self.card._scc.retrieve_data(self.selected_file.fid, tag)
|
||||
|
||||
def retrieve_tags(self):
|
||||
"""Retrieve tags available on BER-TLV EF.
|
||||
|
||||
Returns:
|
||||
list of integer tags contained in EF
|
||||
"""
|
||||
if not isinstance(self.selected_file, BerTlvEF):
|
||||
raise TypeError("Only works with BER-TLV EF")
|
||||
data, sw = self.card._scc.retrieve_data(self.selected_file.fid, 0x5c)
|
||||
tag, length, value = bertlv_parse_one(h2b(data))
|
||||
return list(value)
|
||||
|
||||
def set_data(self, tag:int, data_hex:str):
|
||||
"""Update a TLV/DO with given binary data
|
||||
|
||||
Args:
|
||||
tag : Tag of TLV/DO to be written
|
||||
data_hex : Hex string binary data to be written (value portion)
|
||||
"""
|
||||
if not isinstance(self.selected_file, BerTlvEF):
|
||||
raise TypeError("Only works with BER-TLV EF")
|
||||
return self.card._scc.set_data(self.selected_file.fid, tag, data_hex, conserve=self.conserve_write)
|
||||
|
||||
|
||||
|
||||
|
||||
class FileData(object):
|
||||
|
|
|
@ -113,9 +113,13 @@ def interpret_file_descriptor(in_hex):
|
|||
1: 'transparent',
|
||||
2: 'linear_fixed',
|
||||
6: 'cyclic',
|
||||
0x39: 'ber_tlv',
|
||||
}
|
||||
fdb = in_bin[0]
|
||||
ftype = (fdb >> 3) & 7
|
||||
if fdb & 0xbf == 0x39:
|
||||
fstruct = 0x39
|
||||
else:
|
||||
fstruct = fdb & 7
|
||||
out['shareable'] = True if fdb & 0x40 else False
|
||||
out['file_type'] = ft_dict[ftype] if ftype in ft_dict else ftype
|
||||
|
|
|
@ -89,6 +89,85 @@ def lpad(s:str, l:int, c='f') -> str:
|
|||
def half_round_up(n:int) -> int:
|
||||
return (n + 1)//2
|
||||
|
||||
#########################################################################
|
||||
# poor man's BER-TLV decoder. To be a more sophisticated OO library later
|
||||
#########################################################################
|
||||
|
||||
def bertlv_parse_tag(binary:bytes) -> Tuple[dict, bytes]:
|
||||
"""Parse a single Tag value according to ITU-T X.690 8.1.2
|
||||
Args:
|
||||
binary : binary input data of BER-TLV length field
|
||||
Returns:
|
||||
Tuple of ({class:int, constructed:bool, tag:int}, remainder:bytes)
|
||||
"""
|
||||
cls = binary[0] >> 6
|
||||
constructed = True if binary[0] & 0x20 else False
|
||||
tag = binary[0] & 0x1f
|
||||
if tag <= 30:
|
||||
return ({'class':cls, 'constructed':constructed, 'tag': tag}, binary[1:])
|
||||
else: # multi-byte tag
|
||||
tag = 0
|
||||
i = 1
|
||||
last = False
|
||||
while not last:
|
||||
last = False if binary[i] & 0x80 else True
|
||||
tag <<= 7
|
||||
tag |= binary[i] & 0x7f
|
||||
i += 1
|
||||
return ({'class':cls, 'constructed':constructed, 'tag':tag}, binary[i:])
|
||||
|
||||
def bertlv_parse_len(binary:bytes) -> Tuple[int, bytes]:
|
||||
"""Parse a single Length value according to ITU-T X.690 8.1.3;
|
||||
only the definite form is supported here.
|
||||
Args:
|
||||
binary : binary input data of BER-TLV length field
|
||||
Returns:
|
||||
Tuple of (length, remainder)
|
||||
"""
|
||||
if binary[0] < 0x80:
|
||||
return (binary[0], binary[1:])
|
||||
else:
|
||||
num_len_oct = binary[0] & 0x7f
|
||||
length = 0
|
||||
for i in range(1, 1+num_len_oct):
|
||||
length <<= 8
|
||||
length |= binary[i]
|
||||
return (length, binary[num_len_oct:])
|
||||
|
||||
def bertlv_encode_len(length:int) -> bytes:
|
||||
"""Encode a single Length value according to ITU-T X.690 8.1.3;
|
||||
only the definite form is supported here.
|
||||
Args:
|
||||
length : length value to be encoded
|
||||
Returns:
|
||||
binary output data of BER-TLV length field
|
||||
"""
|
||||
if length < 0x80:
|
||||
return length.to_bytes(1, 'big')
|
||||
elif length <= 0xff:
|
||||
return b'\x81' + length.to_bytes(1, 'big')
|
||||
elif length <= 0xffff:
|
||||
return b'\x82' + length.to_bytes(2, 'big')
|
||||
elif length <= 0xffffff:
|
||||
return b'\x83' + length.to_bytes(3, 'big')
|
||||
elif length <= 0xffffffff:
|
||||
return b'\x84' + length.to_bytes(4, 'big')
|
||||
else:
|
||||
raise ValueError("Length > 32bits not supported")
|
||||
|
||||
def bertlv_parse_one(binary:bytes) -> (dict, int, bytes):
|
||||
"""Parse a single TLV IE at the start of the given binary data.
|
||||
Args:
|
||||
binary : binary input data of BER-TLV length field
|
||||
Returns:
|
||||
Tuple of (tag:dict, len:int, remainder:bytes)
|
||||
"""
|
||||
(tagdict, remainder) = bertlv_parse_tag(binary)
|
||||
(length, remainder) = bertlv_parse_len(remainder)
|
||||
return (tagdict, length, remainder)
|
||||
|
||||
|
||||
|
||||
# IMSI encoded format:
|
||||
# For IMSI 0123456789ABCDE:
|
||||
#
|
||||
|
@ -894,6 +973,10 @@ def tabulate_str_list(str_list, width:int = 79, hspace:int = 2, lspace:int = 1,
|
|||
table.append(format_str_row % tuple(str_list_row))
|
||||
return '\n'.join(table)
|
||||
|
||||
def auto_int(x):
|
||||
"""Helper function for argparse to accept hexadecimal integers."""
|
||||
return int(x, 0)
|
||||
|
||||
class JsonEncoder(json.JSONEncoder):
|
||||
"""Extend the standard library JSONEncoder with support for more types."""
|
||||
def default(self, o):
|
||||
|
|
Loading…
Reference in New Issue