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
diff --git a/pySim/commands.py b/pySim/commands.py
index 33aec12..0b3d9b6 100644
--- a/pySim/commands.py
+++ b/pySim/commands.py
@@ -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 @@
 		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: