Files
pysim/pySim/commands.py
Philipp Maier 42804d7803 commands: pad short data input in update_record()
The method update_record as a "force_len" parameter, which is somewhat
irretatating. Some explainatory comments and a reformat of the if
statement will help to make it more understandable to the api user.

In the non force_len case the method determines the record length from
the select response and throws an exception if the data input does not
match that length. This makes sense if the data input exceeds the
record length of the file but if the data input is less then the record
length the situation is fixable by padding the input with 0xff. This
also a quite common case because in some situation it is not guaranteed
that the data will fill the entire record.

Change-Id: I9a5df0e46c3dd2e87d447c5c01cf15844b0eed07
Related: OS#4963
2021-05-23 10:05:50 +00:00

438 lines
14 KiB
Python

# -*- coding: utf-8 -*-
""" pySim: SIM Card commands according to ISO 7816-4 and TS 11.11
"""
#
# Copyright (C) 2009-2010 Sylvain Munaut <tnt@246tNt.com>
# 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
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from construct import *
from pySim.construct import LV
from pySim.utils import rpad, b2h, h2b, sw_match, bertlv_encode_len
from pySim.exceptions import SwMatchError
class SimCardCommands(object):
def __init__(self, transport):
self._tp = transport
self._cla_byte = "a0"
self.sel_ctrl = "0000"
# Extract a single FCP item from TLV
def __parse_fcp(self, fcp):
# see also: ETSI TS 102 221, chapter 11.1.1.3.1 Response for MF,
# DF or ADF
from pytlv.TLV import TLV
tlvparser = TLV(['82', '83', '84', 'a5', '8a', '8b', '8c', '80', 'ab', 'c6', '81', '88'])
# pytlv is case sensitive!
fcp = fcp.lower()
if fcp[0:2] != '62':
raise ValueError('Tag of the FCP template does not match, expected 62 but got %s'%fcp[0:2])
# Unfortunately the spec is not very clear if the FCP length is
# coded as one or two byte vale, so we have to try it out by
# checking if the length of the remaining TLV string matches
# what we get in the length field.
# See also ETSI TS 102 221, chapter 11.1.1.3.0 Base coding.
exp_tlv_len = int(fcp[2:4], 16)
if len(fcp[4:]) // 2 == exp_tlv_len:
skip = 4
else:
exp_tlv_len = int(fcp[2:6], 16)
if len(fcp[4:]) // 2 == exp_tlv_len:
skip = 6
# Skip FCP tag and length
tlv = fcp[skip:]
return tlvparser.parse(tlv)
# Tell the length of a record by the card response
# USIMs respond with an FCP template, which is different
# from what SIMs responds. See also:
# USIM: ETSI TS 102 221, chapter 11.1.1.3 Response Data
# SIM: GSM 11.11, chapter 9.2.1 SELECT
def __record_len(self, r) -> int:
if self.sel_ctrl == "0004":
tlv_parsed = self.__parse_fcp(r[-1])
file_descriptor = tlv_parsed['82']
# See also ETSI TS 102 221, chapter 11.1.1.4.3 File Descriptor
return int(file_descriptor[4:8], 16)
else:
return int(r[-1][28:30], 16)
# Tell the length of a binary file. See also comment
# above.
def __len(self, r) -> int:
if self.sel_ctrl == "0004":
tlv_parsed = self.__parse_fcp(r[-1])
return int(tlv_parsed['80'], 16)
else:
return int(r[-1][4:8], 16)
def get_atr(self) -> str:
"""Return the ATR of the currently inserted card."""
return self._tp.get_atr()
@property
def cla_byte(self):
return self._cla_byte
@cla_byte.setter
def cla_byte(self, value):
self._cla_byte = value
@property
def sel_ctrl(self):
return self._sel_ctrl
@sel_ctrl.setter
def sel_ctrl(self, value):
self._sel_ctrl = value
def try_select_path(self, dir_list):
""" Try to select a specified path given as list of hex-string FIDs"""
rv = []
if type(dir_list) is not list:
dir_list = [dir_list]
for i in dir_list:
data, sw = self._tp.send_apdu(self.cla_byte + "a4" + self.sel_ctrl + "02" + i)
rv.append((data, sw))
if sw != '9000':
return rv
return rv
def select_path(self, dir_list):
"""Execute SELECT for an entire list/path of FIDs.
Args:
dir_list: list of FIDs representing the path to select
Returns:
list of return values (FCP in hex encoding) for each element of the path
"""
rv = []
if type(dir_list) is not list:
dir_list = [dir_list]
for i in dir_list:
data, sw = self.select_file(i)
rv.append(data)
return rv
def select_file(self, fid:str):
"""Execute SELECT a given file by FID."""
return self._tp.send_apdu_checksw(self.cla_byte + "a4" + self.sel_ctrl + "02" + fid)
def select_adf(self, aid:str):
"""Execute SELECT a given Applicaiton ADF."""
aidlen = ("0" + format(len(aid) // 2, 'x'))[-2:]
return self._tp.send_apdu_checksw(self.cla_byte + "a4" + "0404" + aidlen + aid)
def read_binary(self, ef, length:int=None, offset:int=0):
"""Execute READD BINARY.
Args:
ef : string or list of strings indicating name or path of transparent EF
length : number of bytes to read
offset : byte offset in file from which to start reading
"""
r = self.select_path(ef)
if len(r[-1]) == 0:
return (None, None)
if length is None:
length = self.__len(r) - offset
total_data = ''
while offset < length:
chunk_len = min(255, length-offset)
pdu = self.cla_byte + 'b0%04x%02x' % (offset, chunk_len)
data,sw = self._tp.send_apdu(pdu)
if sw == '9000':
total_data += data
offset += chunk_len
else:
raise ValueError('Failed to read (offset %d)' % (offset))
return total_data, sw
def update_binary(self, ef, data:str, offset:int=0, verify:bool=False, conserve:bool=False):
"""Execute UPDATE BINARY.
Args:
ef : string or list of strings indicating name or path of transparent EF
data : hex string of data to be written
offset : byte offset in file from which to start writing
verify : Whether or not to verify data after write
"""
data_length = len(data) // 2
# Save write cycles by reading+comparing before write
if conserve:
data_current, sw = self.read_binary(ef, data_length, offset)
if data_current == data:
return None, sw
self.select_path(ef)
pdu = self.cla_byte + 'd6%04x%02x' % (offset, data_length) + data
res = self._tp.send_apdu_checksw(pdu)
if verify:
self.verify_binary(ef, data, offset)
return res
def verify_binary(self, ef, data:str, offset:int=0):
"""Verify contents of transparent EF.
Args:
ef : string or list of strings indicating name or path of transparent EF
data : hex string of expected data
offset : byte offset in file from which to start verifying
"""
res = self.read_binary(ef, len(data) // 2, offset)
if res[0].lower() != data.lower():
raise ValueError('Binary verification failed (expected %s, got %s)' % (data.lower(), res[0].lower()))
def read_record(self, ef, rec_no:int):
"""Execute READ RECORD.
Args:
ef : string or list of strings indicating name or path of linear fixed EF
rec_no : record number to read
"""
r = self.select_path(ef)
rec_length = self.__record_len(r)
pdu = self.cla_byte + 'b2%02x04%02x' % (rec_no, rec_length)
return self._tp.send_apdu(pdu)
def update_record(self, ef, rec_no:int, data:str, force_len:bool=False, verify:bool=False,
conserve:bool=False):
res = self.select_path(ef)
if force_len:
# enforce the record length by the actual length of the given data input
rec_length = len(data) // 2
else:
# determine the record length from the select response of the file and pad
# the input data with 0xFF if necessary. In cases where the input data
# exceed we throw an exception.
rec_length = self.__record_len(res)
if (len(data) // 2 > rec_length):
raise ValueError('Data length exceeds record length (expected max %d, got %d)' % (rec_length, len(data) // 2))
elif (len(data) // 2 < rec_length):
data = rpad(data, rec_length * 2)
# Save write cycles by reading+comparing before write
if conserve:
data_current, sw = self.read_record(ef, rec_no)
data_current = data_current[0:rec_length*2]
if data_current == data:
return None, sw
pdu = (self.cla_byte + 'dc%02x04%02x' % (rec_no, rec_length)) + data
res = self._tp.send_apdu_checksw(pdu)
if verify:
self.verify_record(ef, rec_no, data)
return res
def verify_record(self, ef, rec_no:int, data:str):
res = self.read_record(ef, rec_no)
if res[0].lower() != data.lower():
raise ValueError('Record verification failed (expected %s, got %s)' % (data.lower(), res[0].lower()))
def record_size(self, ef):
"""Determine the record size of given file.
Args:
ef : string or list of strings indicating name or path of linear fixed EF
"""
r = self.select_path(ef)
return self.__record_len(r)
def record_count(self, ef):
"""Determine the number of records in given file.
Args:
ef : string or list of strings indicating name or path of linear fixed EF
"""
r = self.select_path(ef)
return self.__len(r) // self.__record_len(r)
def binary_size(self, ef):
"""Determine the size of given transparent file.
Args:
ef : string or list of strings indicating name or path of transparent EF
"""
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:
raise ValueError('Invalid rand')
self.select_path(['3f00', '7f20'])
return self._tp.send_apdu(self.cla_byte + '88000010' + rand)
def authenticate(self, rand:str, autn:str, context='3g'):
"""Execute AUTHENTICATE (USIM/ISIM)."""
# 3GPP TS 31.102 Section 7.1.2.1
AuthCmd3G = Struct('rand'/LV, 'autn'/Optional(LV))
AuthResp3GSyncFail = Struct(Const(b'\xDC'), 'auts'/LV)
AuthResp3GSuccess = Struct(Const(b'\xDB'), 'res'/LV, 'ck'/LV, 'ik'/LV, 'kc'/Optional(LV))
AuthResp3G = Select(AuthResp3GSyncFail, AuthResp3GSuccess)
# build parameters
cmd_data = {'rand': rand, 'autn': autn}
if context == '3g':
p2 = '81'
elif context == 'gsm':
p2 = '80'
(data, sw) = self._tp.send_apdu_constr_checksw(self.cla_byte, '88', '00', p2, AuthCmd3G, cmd_data, AuthResp3G)
if 'auts' in data:
ret = {'synchronisation_failure': data}
else:
ret = {'successful_3g_authentication': data}
return (ret, sw)
def deactivate_file(self):
"""Execute DECATIVATE FILE command as per TS 102 221 Section 11.1.14."""
return self._tp.send_apdu_constr_checksw(self.cla_byte, '04', '00', '00', None, None, None)
def activate_file(self):
"""Execute ACTIVATE FILE command as per TS 102 221 Section 11.1.15."""
return self._tp.send_apdu_constr_checksw(self.cla_byte, '44', '00', '00', None, None, None)
def manage_channel(self, mode='open', lchan_nr=0):
"""Execute MANAGE CHANNEL command as per TS 102 221 Section 11.1.17."""
if mode == 'close':
p1 = 0x80
else:
p1 = 0x00
pdu = self.cla_byte + '70%02x%02x00' % (p1, lchan_nr)
return self._tp.send_apdu_checksw(pdu)
def reset_card(self):
"""Physically reset the card"""
return self._tp.reset_card()
def _chv_process_sw(self, op_name, chv_no, pin_code, sw):
if sw_match(sw, '63cx'):
raise RuntimeError('Failed to %s chv_no 0x%02X with code 0x%s, %i tries left.' %
(op_name, chv_no, b2h(pin_code).upper(), int(sw[3])))
elif (sw != '9000'):
raise SwMatchError(sw, '9000')
def verify_chv(self, chv_no:int, code:str):
"""Verify a given CHV (Card Holder Verification == PIN)"""
fc = rpad(b2h(code), 16)
data, sw = self._tp.send_apdu(self.cla_byte + '2000' + ('%02X' % chv_no) + '08' + fc)
self._chv_process_sw('verify', chv_no, code, sw)
return (data, sw)
def unblock_chv(self, chv_no:int, puk_code:str, pin_code:str):
"""Unblock a given CHV (Card Holder Verification == PIN)"""
fc = rpad(b2h(puk_code), 16) + rpad(b2h(pin_code), 16)
data, sw = self._tp.send_apdu(self.cla_byte + '2C00' + ('%02X' % chv_no) + '10' + fc)
self._chv_process_sw('unblock', chv_no, pin_code, sw)
return (data, sw)
def change_chv(self, chv_no:int, pin_code:str, new_pin_code:str):
"""Change a given CHV (Card Holder Verification == PIN)"""
fc = rpad(b2h(pin_code), 16) + rpad(b2h(new_pin_code), 16)
data, sw = self._tp.send_apdu(self.cla_byte + '2400' + ('%02X' % chv_no) + '10' + fc)
self._chv_process_sw('change', chv_no, pin_code, sw)
return (data, sw)
def disable_chv(self, chv_no:int, pin_code:str):
"""Disable a given CHV (Card Holder Verification == PIN)"""
fc = rpad(b2h(pin_code), 16)
data, sw = self._tp.send_apdu(self.cla_byte + '2600' + ('%02X' % chv_no) + '08' + fc)
self._chv_process_sw('disable', chv_no, pin_code, sw)
return (data, sw)
def enable_chv(self, chv_no:int, pin_code:str):
"""Enable a given CHV (Card Holder Verification == PIN)"""
fc = rpad(b2h(pin_code), 16)
data, sw = self._tp.send_apdu(self.cla_byte + '2800' + ('%02X' % chv_no) + '08' + fc)
self._chv_process_sw('enable', chv_no, pin_code, sw)
return (data, sw)