mirror of
https://gitea.osmocom.org/sim-card/pysim.git
synced 2026-03-17 02:48:34 +03:00
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
438 lines
14 KiB
Python
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)
|