diff --git a/card/FS.py b/card/FS.py new file mode 100644 index 0000000..8ee2443 --- /dev/null +++ b/card/FS.py @@ -0,0 +1,455 @@ +# -*- coding: UTF-8 -*- +""" +card: Library adapted to request (U)SIM cards and other types of telco cards. +Copyright (C) 2010 Benoit Michau + +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, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" + +################################# +# 3GPP SIM and USIM File-System # +# see TS 51.11 for SIM # +# see TS 31.102 for USIM # +################################# + +# (U)SIM file-system dictionnaries +# (absolut_file_address) : 'file_name' + +SIM_FS = { +(0x3F, 0x00) : 'MF', +(0x2F, 0xE2) : 'EF_ICCID', +(0x2F, 0x05) : 'EF_PL', +(0x7F, 0x23) : 'DF_FP-CTS', +(0x7F, 0x22) : 'DF_IS-41', +(0x7F, 0x10) : 'DF_TELECOM', +(0x7F, 0x10, 0x6F, 0x3A) : 'EF_ADN', +(0x7F, 0x10, 0x6F, 0x3B) : 'EF_FDN', +(0x7F, 0x10, 0x6F, 0x3C) : 'EF_SMS', +(0x7F, 0x10, 0x6F, 0x3D) : 'EF_CCP', +(0x7F, 0x10, 0x6F, 0x40) : 'EF_MSISDN', +(0x7F, 0x10, 0x6F, 0x42) : 'EF_SMSP', +(0x7F, 0x10, 0x6F, 0x43) : 'EF_SMSS', +(0x7F, 0x10, 0x6F, 0x44) : 'EF_LND', +(0x7F, 0x10, 0x6F, 0x47) : 'EF_SMSR', +(0x7F, 0x10, 0x6F, 0x49) : 'EF_SDN', +(0x7F, 0x10, 0x6F, 0x4A) : 'EF_EXT1', +(0x7F, 0x10, 0x6F, 0x4B) : 'EF_EXT2', +(0x7F, 0x10, 0x6F, 0x4C) : 'EF_EXT3', +(0x7F, 0x10, 0x6F, 0x4D) : 'EF_BDN', +(0x7F, 0x10, 0x6F, 0x4E) : 'EF_EXT4', +(0x7F, 0x10, 0x6F, 0x58) : 'EF_CMI', +(0x7F, 0x10, 0x6F, 0x4F) : 'EF_ECCP', +(0x7F, 0x10, 0x5F, 0x50) : 'DF_GRAPHICS', +(0x7F, 0x10, 0x5F, 0x50, 0x4F, 0x20) : 'EF_IMG', +(0x7F, 0x20) : 'DF_GSM', +(0x7F, 0x20, 0x5F, 0x3C) : 'DF_MExE', +(0x7F, 0x20, 0x5F, 0x3C, 0x4F, 0x40) : 'EF_MExE-ST', +(0x7F, 0x20, 0x5F, 0x3C, 0x4F, 0x41) : 'EF_ORPK', +(0x7F, 0x20, 0x5F, 0x3C, 0x4F, 0x42) : 'EF_ARPK', +(0x7F, 0x20, 0x5F, 0x3C, 0x4F, 0x43) : 'EF_TPRPK', +(0x7F, 0x20, 0x5F, 0x30) : 'DF_IRIDIUM', +(0x7F, 0x20, 0x5F, 0x31) : 'DF_GLOBALSTAR', +(0x7F, 0x20, 0x5F, 0x32) : 'DF_ICO', +(0x7F, 0x20, 0x5F, 0x33) : 'DF_ACeS', +(0x7F, 0x20, 0x5F, 0x40) : 'DF_EIA/TIA-553', +(0x7F, 0x20, 0x5F, 0x60) : 'DF_CTS', +(0x7F, 0x20, 0x5F, 0x70) : 'DF_SoLSA', +(0x7F, 0x20, 0x5F, 0x70, 0x4F, 0x30) : 'EF_SAI', +(0x7F, 0x20, 0x5F, 0x70, 0x4F, 0x31) : 'EF_SLL', +(0x7F, 0x20, 0x6F, 0x05) : 'EF_LP', +(0x7F, 0x20, 0x6F, 0x07) : 'EF_IMSI', +(0x7F, 0x20, 0x6F, 0x20) : 'EF_Kc', +(0x7F, 0x20, 0x6F, 0x2C) : 'EF_DCK', +(0x7F, 0x20, 0x6F, 0x30) : 'EF_PLMNsel', +(0x7F, 0x20, 0x6F, 0x31) : 'EF_HPLMN', +(0x7F, 0x20, 0x6F, 0x32) : 'EF_CNL', +(0x7F, 0x20, 0x6F, 0x37) : 'EF_ACMmax', +(0x7F, 0x20, 0x6F, 0x38) : 'EF_SST', +(0x7F, 0x20, 0x6F, 0x39) : 'EF_ACM', +(0x7F, 0x20, 0x6F, 0x3E) : 'EF_GID1', +(0x7F, 0x20, 0x6F, 0x3F) : 'EF_GID2', +(0x7F, 0x20, 0x6F, 0x41) : 'EF_PUCT', +(0x7F, 0x20, 0x6F, 0x45) : 'EF_CBMI', +(0x7F, 0x20, 0x6F, 0x46) : 'EF_SPN', +(0x7F, 0x20, 0x6F, 0x48) : 'EF_CBMID', +(0x7F, 0x20, 0x6F, 0x74) : 'EF_BCCH', +(0x7F, 0x20, 0x6F, 0x78) : 'EF_ACC', +(0x7F, 0x20, 0x6F, 0x7B) : 'EF_FPLMN', +(0x7F, 0x20, 0x6F, 0x7E) : 'EF_LOCI', +(0x7F, 0x20, 0x6F, 0xAD) : 'EF_AD', +(0x7F, 0x20, 0x6F, 0xAE) : 'EF_PHASE', +(0x7F, 0x20, 0x6F, 0xB1) : 'EF_VGCS', +(0x7F, 0x20, 0x6F, 0xB2) : 'EF_VGCSS', +(0x7F, 0x20, 0x6F, 0xB3) : 'EF_VBS', +(0x7F, 0x20, 0x6F, 0xB4) : 'EF_VBSS', +(0x7F, 0x20, 0x6F, 0xB5) : 'EF_eMLPP', +(0x7F, 0x20, 0x6F, 0xB6) : 'EF_AAeM', +(0x7F, 0x20, 0x6F, 0xB7) : 'EF_ECC', +(0x7F, 0x20, 0x6F, 0x50) : 'EF_CBMIR', +(0x7F, 0x20, 0x6F, 0x51) : 'EF_NIA', +(0x7F, 0x20, 0x6F, 0x52) : 'EF_KcGPRS', +(0x7F, 0x20, 0x6F, 0x53) : 'EF_LOCIGPRS', +(0x7F, 0x20, 0x6F, 0x54) : 'EF_SUME', +(0x7F, 0x20, 0x6F, 0x60) : 'EF_PLMNwAcT', +(0x7F, 0x20, 0x6F, 0x61) : 'EF_OPLMNwAcT', +(0x7F, 0x20, 0x6F, 0x62) : 'EF_HPLMNAcT', +(0x7F, 0x20, 0x6F, 0x63) : 'EF_CPBCCH', +(0x7F, 0x20, 0x6F, 0x64) : 'EF_INVSCAN', +(0x7F, 0x20, 0x6F, 0xC5) : 'EF_PNN', +(0x7F, 0x20, 0x6F, 0xC6) : 'EF_OPL', +(0x7F, 0x20, 0x6F, 0xC7) : 'EF_MBDN', +(0x7F, 0x20, 0x6F, 0xC8) : 'EF_EXT6', +(0x7F, 0x20, 0x6F, 0xC9) : 'EF_MBI', +(0x7F, 0x20, 0x6F, 0xCA) : 'EF_MWIS', +(0x7F, 0x20, 0x6F, 0xCB) : 'EF_CFIS', +(0x7F, 0x20, 0x6F, 0xCC) : 'EF_EXT7', +(0x7F, 0x20, 0x6F, 0xCD) : 'EF_SPDI', +(0x7F, 0x20, 0x6F, 0xCE) : 'EF_MMSN', +(0x7F, 0x20, 0x6F, 0xCF) : 'EF_EXT8', +(0x7F, 0x20, 0x6F, 0xD0) : 'EF_MMSICP', +(0x7F, 0x20, 0x6F, 0xD1) : 'EF_MMSUP', +(0x7F, 0x20, 0x6F, 0xD2) : 'EF_MMSUCP', +} + +USIM_FS = { +(0x3F, 0x00) : 'MF', +(0x2F, 0x00) : 'EF_DIR', +(0x2F, 0x05) : 'EF_PL', +(0x2F, 0x06) : 'EF_ARR', +(0x2F, 0xE2) : 'EF_ICCID', +(0x7F, 0x20) : 'DF_GSM', +(0x7F, 0x10) : 'DF_TELECOM', +(0x7F, 0x10, 0x6F, 0x06) : 'EF_ARR', +(0x7F, 0x10, 0x6F, 0x3A) : 'EF_ADN', +(0x7F, 0x10, 0x6F, 0x3B) : 'EF_FDN', +(0x7F, 0x10, 0x6F, 0x3C) : 'EF_SMS', +(0x7F, 0x10, 0x6F, 0x4F) : 'EF_ECCP', +(0x7F, 0x10, 0x6F, 0x40) : 'EF_MSISDN', +(0x7F, 0x10, 0x6F, 0x42) : 'EF_SMSP', +(0x7F, 0x10, 0x6F, 0x43) : 'EF_SMSS', +(0x7F, 0x10, 0x6F, 0x44) : 'EF_LND', +(0x7F, 0x10, 0x6F, 0x47) : 'EF_SMSR', +(0x7F, 0x10, 0x6F, 0x49) : 'EF_SDN', +(0x7F, 0x10, 0x6F, 0x4A) : 'EF_EXT1', +(0x7F, 0x10, 0x6F, 0x4B) : 'EF_EXT2', +(0x7F, 0x10, 0x6F, 0x4C) : 'EF_EXT3', +(0x7F, 0x10, 0x6F, 0x4D) : 'EF_BDN', +(0x7F, 0x10, 0x6F, 0x4E) : 'EF_EXT4', +(0x7F, 0x10, 0x6F, 0x53) : 'EF_RMA', +(0x7F, 0x10, 0x6F, 0x54) : 'EF_SUME', +(0x7F, 0x10, 0x5F, 0x50) : 'DF_GRAPHICS', +(0x7F, 0x10, 0x5F, 0x50, 0x4F, 0x20) : 'EF_IMG', +#(0x7F, 0x10, 0x5F, 0x50, 0x4F, 0xXX) : 'EF_IIDFn', +(0x5F, 0x3A) : 'DF_PHONEBOOK', +(0x5F, 0x3A, 0x4F, 0x30) : 'EF_PBR', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_IAP', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_ADN', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_EXT1', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_PBC', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_GRP', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_AAS', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_GAS', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_ANR', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_SNE', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_CCP1', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_UID', +(0x5F, 0x3A, 0x4F, 0x22) : 'EF_PSC', +(0x5F, 0x3A, 0x4F, 0x23) : 'EF_CC', +(0x5F, 0x3A, 0x4F, 0x24) : 'EF_PUID', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_EMAIL', +(0x5F, 0x3B) : 'DF_MULTIMEDIA', +(0x5F, 0x3B, 0x4F, 0x47) : 'EF_MML', +(0x5F, 0x3B, 0x4F, 0x48) : 'EF_MMDF', +} + +# this MF_FS is a trick that concatenate SIM_FS and USIM_FS +# useful when bruteforcing the FS under MF whatever the type of card selected +MF_FS = { +(0x3F, 0x00) : 'MF', +(0x2F, 0x00) : 'EF_DIR', +(0x2F, 0x05) : 'EF_ELP', +(0x2F, 0x06) : 'EF_ARR', +(0x2F, 0xE2) : 'EF_ICCID', +(0x5F, 0x3A) : 'DF_PHONEBOOK', +(0x5F, 0x3A, 0x4F, 0x30) : 'EF_PBR', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_IAP', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_ADN', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_EXT1', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_PBC', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_GRP', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_AAS', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_GAS', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_ANR', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_SNE', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_CCP1', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_UID', +(0x5F, 0x3A, 0x4F, 0x22) : 'EF_PSC', +(0x5F, 0x3A, 0x4F, 0x23) : 'EF_CC', +(0x5F, 0x3A, 0x4F, 0x24) : 'EF_PUID', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_EMAIL', +(0x5F, 0x3B) : 'DF_MULTIMEDIA', +(0x5F, 0x3B, 0x4F, 0x47) : 'EF_MML', +(0x5F, 0x3B, 0x4F, 0x48) : 'EF_MMDF', +(0x7F, 0x10) : 'DF_TELECOM', +(0x7F, 0x10, 0x6F, 0x06) : 'EF_ARR', +(0x7F, 0x10, 0x6F, 0x3A) : 'EF_ADN', +(0x7F, 0x10, 0x6F, 0x3B) : 'EF_FDN', +(0x7F, 0x10, 0x6F, 0x3C) : 'EF_SMS', +(0x7F, 0x10, 0x6F, 0x3D) : 'EF_CCP', +(0x7F, 0x10, 0x6F, 0x40) : 'EF_MSISDN', +(0x7F, 0x10, 0x6F, 0x42) : 'EF_SMSP', +(0x7F, 0x10, 0x6F, 0x43) : 'EF_SMSS', +(0x7F, 0x10, 0x6F, 0x44) : 'EF_LND', +(0x7F, 0x10, 0x6F, 0x47) : 'EF_SMSR', +(0x7F, 0x10, 0x6F, 0x49) : 'EF_SDN', +(0x7F, 0x10, 0x6F, 0x4A) : 'EF_EXT1', +(0x7F, 0x10, 0x6F, 0x4B) : 'EF_EXT2', +(0x7F, 0x10, 0x6F, 0x4C) : 'EF_EXT3', +(0x7F, 0x10, 0x6F, 0x4D) : 'EF_BDN', +(0x7F, 0x10, 0x6F, 0x4E) : 'EF_EXT4', +(0x7F, 0x10, 0x6F, 0x4F) : 'EF_ECCP', +(0x7F, 0x10, 0x5F, 0x50) : 'DF_GRAPHICS', +(0x7F, 0x10, 0x5F, 0x50, 0x4F, 0x20) : 'EF_IMG', +(0x7F, 0x10, 0x6F, 0x53) : 'EF_RMA', +(0x7F, 0x10, 0x6F, 0x54) : 'EF_SUME', +(0x7F, 0x10, 0x6F, 0x58) : 'EF_CMI', +(0x7F, 0x20) : 'DF_GSM', +(0x7F, 0x20, 0x5F, 0x3C) : 'DF_MExE', +(0x7F, 0x20, 0x5F, 0x3C, 0x4F, 0x40) : 'EF_MExE-ST', +(0x7F, 0x20, 0x5F, 0x3C, 0x4F, 0x41) : 'EF_ORPK', +(0x7F, 0x20, 0x5F, 0x3C, 0x4F, 0x42) : 'EF_ARPK', +(0x7F, 0x20, 0x5F, 0x3C, 0x4F, 0x43) : 'EF_TPRPK', +(0x7F, 0x20, 0x5F, 0x30) : 'DF_IRIDIUM', +(0x7F, 0x20, 0x5F, 0x31) : 'DF_GLOBALSTAR', +(0x7F, 0x20, 0x5F, 0x32) : 'DF_ICO', +(0x7F, 0x20, 0x5F, 0x33) : 'DF_ACeS', +(0x7F, 0x20, 0x5F, 0x40) : 'DF_EIA/TIA-553', +(0x7F, 0x20, 0x5F, 0x60) : 'DF_CTS', +(0x7F, 0x20, 0x5F, 0x70) : 'DF_SoLSA', +(0x7F, 0x20, 0x5F, 0x70, 0x4F, 0x30) : 'EF_SAI', +(0x7F, 0x20, 0x5F, 0x70, 0x4F, 0x31) : 'EF_SLL', +(0x7F, 0x20, 0x6F, 0x05) : 'EF_LP', +(0x7F, 0x20, 0x6F, 0x07) : 'EF_IMSI', +(0x7F, 0x20, 0x6F, 0x20) : 'EF_Kc', +(0x7F, 0x20, 0x6F, 0x2C) : 'EF_DCK', +(0x7F, 0x20, 0x6F, 0x30) : 'EF_PLMNsel', +(0x7F, 0x20, 0x6F, 0x31) : 'EF_HPLMN', +(0x7F, 0x20, 0x6F, 0x32) : 'EF_CNL', +(0x7F, 0x20, 0x6F, 0x37) : 'EF_ACMmax', +(0x7F, 0x20, 0x6F, 0x38) : 'EF_SST', +(0x7F, 0x20, 0x6F, 0x39) : 'EF_ACM', +(0x7F, 0x20, 0x6F, 0x3E) : 'EF_GID1', +(0x7F, 0x20, 0x6F, 0x3F) : 'EF_GID2', +(0x7F, 0x20, 0x6F, 0x41) : 'EF_PUCT', +(0x7F, 0x20, 0x6F, 0x45) : 'EF_CBMI', +(0x7F, 0x20, 0x6F, 0x46) : 'EF_SPN', +(0x7F, 0x20, 0x6F, 0x48) : 'EF_CBMID', +(0x7F, 0x20, 0x6F, 0x74) : 'EF_BCCH', +(0x7F, 0x20, 0x6F, 0x78) : 'EF_ACC', +(0x7F, 0x20, 0x6F, 0x7B) : 'EF_FPLMN', +(0x7F, 0x20, 0x6F, 0x7E) : 'EF_LOCI', +(0x7F, 0x20, 0x6F, 0xAD) : 'EF_AD', +(0x7F, 0x20, 0x6F, 0xAE) : 'EF_PHASE', +(0x7F, 0x20, 0x6F, 0xB1) : 'EF_VGCS', +(0x7F, 0x20, 0x6F, 0xB2) : 'EF_VGCSS', +(0x7F, 0x20, 0x6F, 0xB3) : 'EF_VBS', +(0x7F, 0x20, 0x6F, 0xB4) : 'EF_VBSS', +(0x7F, 0x20, 0x6F, 0xB5) : 'EF_eMLPP', +(0x7F, 0x20, 0x6F, 0xB6) : 'EF_AAeM', +(0x7F, 0x20, 0x6F, 0xB7) : 'EF_ECC', +(0x7F, 0x20, 0x6F, 0x50) : 'EF_CBMIR', +(0x7F, 0x20, 0x6F, 0x51) : 'EF_NIA', +(0x7F, 0x20, 0x6F, 0x52) : 'EF_KcGPRS', +(0x7F, 0x20, 0x6F, 0x53) : 'EF_LOCIGPRS', +(0x7F, 0x20, 0x6F, 0x54) : 'EF_SUME', +(0x7F, 0x20, 0x6F, 0x60) : 'EF_PLMNwAcT', +(0x7F, 0x20, 0x6F, 0x61) : 'EF_OPLMNwAcT', +(0x7F, 0x20, 0x6F, 0x62) : 'EF_HPLMNAcT', +(0x7F, 0x20, 0x6F, 0x63) : 'EF_CPBCCH', +(0x7F, 0x20, 0x6F, 0x64) : 'EF_INVSCAN', +(0x7F, 0x20, 0x6F, 0xC5) : 'EF_PNN', +(0x7F, 0x20, 0x6F, 0xC6) : 'EF_OPL', +(0x7F, 0x20, 0x6F, 0xC7) : 'EF_MBDN', +(0x7F, 0x20, 0x6F, 0xC8) : 'EF_EXT6', +(0x7F, 0x20, 0x6F, 0xC9) : 'EF_MBI', +(0x7F, 0x20, 0x6F, 0xCA) : 'EF_MWIS', +(0x7F, 0x20, 0x6F, 0xCB) : 'EF_CFIS', +(0x7F, 0x20, 0x6F, 0xCC) : 'EF_EXT7', +(0x7F, 0x20, 0x6F, 0xCD) : 'EF_SPDI', +(0x7F, 0x20, 0x6F, 0xCE) : 'EF_MMSN', +(0x7F, 0x20, 0x6F, 0xCF) : 'EF_EXT8', +(0x7F, 0x20, 0x6F, 0xD0) : 'EF_MMSICP', +(0x7F, 0x20, 0x6F, 0xD1) : 'EF_MMSUP', +(0x7F, 0x20, 0x6F, 0xD2) : 'EF_MMSUCP', +(0x7F, 0x23) : 'DF_FP-CTS', +(0x7F, 0x22) : 'DF_IS-41', +} + +USIM_app_FS = { +(0x5F, 0x40) : 'DF_WLAN', +(0x5F, 0x40, 0x4F, 0x41) : 'EF_Pseudo', +(0x5F, 0x40, 0x4F, 0x42) : 'EF_UPLMNWLAN', +(0x5F, 0x40, 0x4F, 0x43) : 'EF_0PLMNWLAN', +(0x5F, 0x40, 0x4F, 0x44) : 'EF_USSIDL', +(0x5F, 0x40, 0x4F, 0x45) : 'EF_OSSIDL', +(0x5F, 0x40, 0x4F, 0x46) : 'EF_WRI', +(0x5F, 0x70) : 'DF_SoLSA', +(0x5F, 0x70, 0x4F, 0x30) : 'EF_SAI', +(0x5F, 0x70, 0x4F, 0x31) : 'EF_SLL', +(0x5F, 0x3C) : 'DF_MExE', +(0x5F, 0x3C, 0x4F, 0x40) : 'EF_MExE-ST', +(0x5F, 0x3C, 0x4F, 0x41) : 'EF_ORPK', +(0x5F, 0x3C, 0x4F, 0x42) : 'EF_ARPK', +(0x5F, 0x3C, 0x4F, 0x43) : 'EF_TPRK', +#(0x5F, 0x3C, 0x4F, 0xXX) : 'EF_TKCDF', +(0x5F, 0x3B) : 'DF_GSM-ACCESS', +(0x5F, 0x3B, 0x4F, 0x20) : 'EF_Kc', +(0x5F, 0x3B, 0x4F, 0x52) : 'EF_KcGPRS', +(0x5F, 0x3B, 0x4F, 0x63) : 'EF_CPBCCH', +(0x5F, 0x3B, 0x4F, 0x64) : 'EF_invSCAN', +(0x5F, 0x3A) : 'DF_PHONEBOOK', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_UID', +(0x5F, 0x3A, 0x4F, 0x22) : 'EF_PSC', +(0x5F, 0x3A, 0x4F, 0x23) : 'EF_CC', +(0x5F, 0x3A, 0x4F, 0x24) : 'EF_PUID', +(0x5F, 0x3A, 0x4F, 0x30) : 'EF_PBR', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_CCP1', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_IAP', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_ADN', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_EXT1', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_PBC', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_GRP', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_AAS', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_GAS', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_ANR', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_SNE', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_EMAIL', +(0x6F, 0x05) : 'EF_LI', +(0x6F, 0x06) : 'EF_ARR', +(0x6F, 0x07) : 'EF_IMSI', +(0x6F, 0x08) : 'EF_Keys', +(0x6F, 0x09) : 'EF_KeysPS', +(0x6F, 0x2C) : 'EF_DCK', +(0x6F, 0x31) : 'EF_HPLMN', +(0x6F, 0x32) : 'EF_CNL', +(0x6F, 0x37) : 'EF_ACMmax', +(0x6F, 0x38) : 'EF_UST', +(0x6F, 0x39) : 'EF_ACM', +(0x6F, 0x3B) : 'EF_FDN', +(0x6F, 0x3C) : 'EF_SMS', +(0x6F, 0x3E) : 'EF_GID1', +(0x6F, 0x3F) : 'EF_GID2', +(0x6F, 0x40) : 'EF_MSISDN', +(0x6F, 0x41) : 'EF_PUCT', +(0x6F, 0x42) : 'EF_SMSP', +(0x6F, 0x43) : 'EF_SMSS', +(0x6F, 0x45) : 'EF_CBMI', +(0x6F, 0x46) : 'EF_SPN', +(0x6F, 0x47) : 'EF_SMSR', +(0x6F, 0x48) : 'EF_CBMID', +(0x6F, 0x49) : 'EF_SDN', +(0x6F, 0x4B) : 'EF_EXT2', +(0x6F, 0x4C) : 'EF_EXT3', +(0x6F, 0x4D) : 'EF_BDN', +(0x6F, 0x4E) : 'EF_EXT5', +(0x6F, 0x50) : 'EF_CBMIR', +(0x6F, 0x55) : 'EF_EXT4', +(0x6F, 0x56) : 'EF_EST', +(0x6F, 0x57) : 'EF_ACL', +(0x6F, 0x58) : 'EF_CMI', +(0x6F, 0x5B) : 'EF_START-HFN', +(0x6F, 0x5C) : 'EF_THRESHOLD', +(0x6F, 0x60) : 'EF_PLMNwAcT', +(0x6F, 0x61) : 'EF_OPLMNwAcT', +(0x6F, 0x62) : 'EF_HPLMNwAcT', +(0x6F, 0xD9) : 'EF_EHPLMN', +(0x6F, 0x73) : 'EF_PSLOCI', +(0x6F, 0x78) : 'EF_ACC', +(0x6F, 0x7B) : 'EF_FPLMN', +(0x6F, 0x7E) : 'EF_LOCI', +(0x6F, 0x80) : 'EF_ICI', +(0x6F, 0x81) : 'EF_OCI', +(0x6F, 0x82) : 'EF_ICT', +(0x6F, 0x83) : 'EF_OCT', +(0x6F, 0xAD) : 'EF_AD', +(0x6F, 0xB5) : 'EF_eMLPP', +(0x6F, 0xB6) : 'EF_AAeM', +(0x6F, 0xB7) : 'EF_ECC', +(0x6F, 0xC3) : 'EF_Hiddenkey', +(0x6F, 0xC4) : 'EF_NETPAR', +(0x6F, 0xC5) : 'EF_PNN', +(0x6F, 0xC6) : 'EF_OPL', +(0x6F, 0xC7) : 'EF_MBDN', +(0x6F, 0xC8) : 'EF_EXT6', +(0x6F, 0xC9) : 'EF_MBI', +(0x6F, 0xCA) : 'EF_MWIS', +(0x6F, 0xCB) : 'EF_CFIS', +(0x6F, 0xCC) : 'EF_EXT7', +(0x6F, 0xCD) : 'EF_SPDI', +(0x6F, 0xCE) : 'EF_MMSN', +(0x6F, 0xCF) : 'EF_EXT8', +(0x6F, 0xD0) : 'EF_MMSICP', +(0x6F, 0xD1) : 'EF_MMSUP', +(0x6F, 0xD2) : 'EF_MMSUCP', +(0x6F, 0xD3) : 'EF_NIA', +(0x6F, 0x4F) : 'EF_CCP2', +(0x6F, 0xB1) : 'EF_VGCS', +(0x6F, 0xB2) : 'EF_VGCSS', +(0x6F, 0xB3) : 'EF_VBS', +(0x6F, 0xB4) : 'EF_VBSS', +(0x6F, 0xD4) : 'EF_VGCSCA', +(0x6F, 0xD5) : 'EF_VBSCA', +(0x6F, 0xD6) : 'EF_GBAP', +(0x6F, 0xD7) : 'EF_MSK', +(0x6F, 0xD8) : 'EF_MUK', +(0x6F, 0xDA) : 'EF_GBANL', +(0x6F, 0xDB) : 'EF_EHPLMNPI', +(0x6F, 0xDC) : 'EF_LRPLMNSI', +(0x6F, 0xDD) : 'EF_NAFKCA', +(0x6F, 0xDE) : 'EF_SPNI', +(0x6F, 0xDF) : 'EF_PNNI', +} + +# Actually, those DF can be under MF, ADF_USIM or DF_TELECOM: +DF_PHONEBOOK = { +(0x5F, 0x3A) : 'DF_PHONEBOOK', +(0x5F, 0x3A, 0x4F) : 'EF_UID', +(0x5F, 0x3A, 0x4F, 0x22) : 'EF_PSC', +(0x5F, 0x3A, 0x4F, 0x23) : 'EF_CC', +(0x5F, 0x3A, 0x4F, 0x24) : 'EF_PUID', +(0x5F, 0x3A, 0x4F, 0x30) : 'EF_PBR', +(0x5F, 0x3A, 0x4F) : 'EF_CCP1', +(0x5F, 0x3A, 0x4F) : 'EF_IAP', +(0x5F, 0x3A, 0x4F) : 'EF_ADN', +(0x5F, 0x3A, 0x4F) : 'EF_EXT1', +(0x5F, 0x3A, 0x4F) : 'EF_PBC', +(0x5F, 0x3A, 0x4F) : 'EF_GRP', +(0x5F, 0x3A, 0x4F) : 'EF_AAS', +(0x5F, 0x3A, 0x4F) : 'EF_GAS', +(0x5F, 0x3A, 0x4F) : 'EF_ANR', +(0x5F, 0x3A, 0x4F) : 'EF_SNE', +(0x5F, 0x3A, 0x4F) : 'EF_EMAIL', +} + +DF_GRAPHICS = { +(0x5F, 0x50) : 'DF_GRAPHICS', +(0x5F, 0x50, 0x4F, 0x20) : 'EF_IMG', +} + +DF_MULTIMEDIA = { +(0x5F, 0x3B) : 'DF_MULTIMEDIA', +(0x5F, 0x3B, 0x4F, 0x47) : 'EF_MML', +(0x5F, 0x3B, 0x4F, 0x48) : 'EF_MMDF', +} diff --git a/card/ICC.py b/card/ICC.py new file mode 100644 index 0000000..9f94022 --- /dev/null +++ b/card/ICC.py @@ -0,0 +1,1704 @@ +# -*- coding: UTF-8 -*- +""" +card: Library adapted to request (U)SIM cards and other types of telco cards. +Copyright (C) 2010 Benoit Michau + +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, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" + +################################# +# Python library to work on +# smartcard defined with ISO 7816 +# +# Specially designed SIM and USIM class +# for ETSI / 3GPP cards +# +# needs pyscard from: +# http://pyscard.sourceforge.net/ +################################# + +# classic python modules +import os +import re + +# smartcard python modules from pyscard +from smartcard.CardType import AnyCardType +from smartcard.CardRequest import CardRequest +from smartcard.CardConnection import CardConnection +from smartcard.CardConnectionObserver import ConsoleCardConnectionObserver +from smartcard.ATR import ATR +from smartcard.Exceptions import CardConnectionException +from smartcard.util import toHexString + +from card.utils import * + +########################################################### +# ISO7816 class with attributes and methods as defined +# by ISO-7816 part 4 standard for smartcard +########################################################### + +class ISO7816(object): + ''' + define attributes, methods and facilities for ISO-7816-4 standard smartcard + + use self.dbg = 1 or more to print live debugging information + standard instructions codes available in "INS_dic" class dictionnary + standard file tags available in "file_tags" class dictionnary + ''' + + dbg = 0 + + INS_dic = { + 0x04 : 'DEACTIVATE FILE', + 0x0C : 'ERASE RECORD(S)', + 0x0E : 'ERASE BINARY', + 0x0F : 'ERASE BINARY', + 0x10 : 'TERMINAL PROFILE', + 0x12 : 'FETCH', + 0x14 : 'TERMINAL RESPONSE', + 0x20 : 'VERIFY', + 0x21 : 'VERIFY', + 0x22 : 'MANAGE SECURITY ENVIRONMENT', + 0x24 : 'CHANGE PIN', + 0x26 : 'DISABLE PIN', + 0x28 : 'ENABLE PIN', + 0x2A : 'PERFORM SECURITY OPERATION', + 0x2C : 'UNBLOCK PIN', + 0x32 : 'INCREASE', + 0x44 : 'ACTIVATE FILE', + 0x46 : 'GENERATE ASYMETRIC KEY PAIR', + 0x70 : 'MANAGE CHANNEL', + 0x73 : 'MANAGE SECURE CHANNEL', + 0x75 : 'TRANSACT DATA', + 0x82 : 'EXTERNAL AUTHENTICATE', + 0x84 : 'GET CHALLENGE', + 0x86 : 'GENERAL AUTHENTICATE', + 0x87 : 'GENERAL AUTHENTICATE', + 0x88 : 'INTERNAL AUTHENTICATE', + 0x89 : 'AUTHENTICATE', + 0x99 : 'PROGRAM SYSMO_USIM', + 0xA0 : 'SEARCH BINARY', + 0xA1 : 'SEARCH BINARY', + 0xA2 : 'SEARCH RECORD', + 0xA4 : 'SELECT FILE', + 0xA8 : 'GET PROCESSING OPTIONS', + 0xAA : 'TERMINAL CAPABILITY', + 0xB0 : 'READ BINARY', + 0xB1 : 'READ BINARY', + 0xB2 : 'READ RECORD(S)', + 0xB3 : 'READ RECORD(S)', + 0xC0 : 'GET RESPONSE', + 0xC2 : 'ENVELOPE', + 0xC3 : 'ENVELOPE', + 0xCA : 'RETRIEVE DATA', + 0xCB : 'RETRIEVE DATA', + 0xD2 : 'WRITE RECORD', + 0xD6 : 'UPDATE BINARY', + 0xD7 : 'UPDATE BINARY', + 0xDA : 'SET DATA', + 0xDB : 'SET DATA', + 0xDC : 'UPDATE RECORD', + 0xDD : 'UPDATE RECORD', + 0xE0 : 'CREATE FILE', + 0xE2 : 'APPEND RECORD', + 0xE4 : 'DELETE FILE', + 0xE6 : 'TERMINATE DF', + 0xE8 : 'TERMINATE EF', + 0xF2 : 'STATUS', + 0xFE : 'TERMINATE CARD USAGE', + } + + file_tags = { + 0x80 : 'Size', + 0x81 : 'Length', + 0x82 : 'File Descriptor', + 0x83 : 'File Identifier', + 0x84 : 'DF Name', + 0x85 : 'Proprietary no-BERTLV', + 0x86 : 'Proprietary Security Attribute', + 0x87 : 'EF with FCI extension', + 0x88 : 'Short File Identifier', + 0x8A : 'Life Cycle Status', + 0x8B : 'Security Attributes ref to expanded', + 0x8C : 'Security Attributes compact', + 0x8D : 'EF with Security Environment', + 0x8E : 'Channel Security Attribute', + 0xA0 : 'Security Attribute for DO', + 0xA1 : 'Proprietary Security Attribute', + 0xA2 : 'DO Pairs', + 0xA5 : 'Proprietary BERTLV', + 0xAB : 'Security Attribute expanded', + } + + def __init__(self, CLA=0x00): + ''' + connect smartcard and defines class CLA code for communication + uses "pyscard" library services + + creates self.CLA attribute with CLA code + and self.coms attribute with associated "apdu_stack" instance + ''' + cardtype = AnyCardType() + cardrequest = CardRequest(timeout=1, cardType=cardtype) + self.cardservice = cardrequest.waitforcard() + self.cardservice.connection.connect() + self.reader = self.cardservice.connection.getReader() + self.ATR = self.cardservice.connection.getATR() + + #observer = ConsoleCardConnectionObserver() + #self.cardservice.connection.addObserver(observer) + + self.CLA = CLA + self.coms = apdu_stack() + + def disconnect(self): + ''' + disconnect smartcard: stops the session + uses "pyscard" library service + ''' + self.cardservice.connection.disconnect() + + def define_class(self, CLA=0x00): + ''' + define smartcard class attribute for APDU command + override CLA value defined in class initialization + ''' + self.CLA = CLA + + def ATR_scan(self, smlist_file="/usr/share/pcsc/smartcard_list.txt"): + ''' + print smartcard info retrieved from AnswerToReset + thanks to pyscard routine + + if pcsc_scan is installed, + use the signature file passed as argument for guessing the card + ''' + print('\nsmartcard reader: %s' % self.reader) + if self.ATR != None: + print('\nsmart card ATR is: %s' % toHexString(self.ATR)) + print('ATR analysis: ') + print('%s' % ATR(self.ATR).dump()) + print('\nhistorical bytes: %s' \ + % toHexString(ATR(self.ATR).getHistoricalBytes())) + ATRcs = ATR(self.ATR).getChecksum() + if ATRcs : + print('checksum: 0x%X' % ATRcs) + else: + print('no ATR checksum') + print('\nusing pcsc_scan ATR list file: %s' % smlist_file) + if os.path.exists(smlist_file): + smlist = open(smlist_file).readlines() + ATRre = re.compile('(^3[BF]){1}.{1,}$') + ATRfinger = '' + j = 1 + for i in range(len(smlist)): + if ATRre.match(smlist[i]): + if re.compile(smlist[i][:len(smlist[i])-1]).\ + match(toHexString(self.ATR)): + while re.compile('\t.{1,}').match(smlist[i+j]): + ATRfinger += smlist[i+j][1:] + j += j + if ATRfinger == '' : + print('no ATR fingerprint found in file: %s' % smlist_file) + else: + print('smartcard ATR fingerprint:\n%s' % ATRfinger) + else: + print('%s file not found' % smlist_file) + + @staticmethod + def sw_status(sw1, sw2): + ''' + sw_status(sw1=int, sw2=int) -> string + + SW status bytes interpretation as defined in ISO-7816 part 4 standard + helps to speak and understand with the smartcard! + ''' + status = 'undefined status' + if sw1 == 0x90 and sw2 == 0x00: status = 'normal processing: ' \ + 'command accepted: no further qualification' + elif sw1 == 0x61: status = 'normal processing: %i bytes ' \ + 'still available' % sw2 + elif sw1 == 0x62: + status = 'warning processing: state of non-volatile '\ + 'memory unchanged' + if sw2 == 0x00: status += ': no information given' + elif sw2 == 0x81: status += ': part of returned data may' \ + 'be corrupted' + elif sw2 == 0x82: status += ': end of file/record reached ' \ + 'before reading Le bytes' + elif sw2 == 0x83: status += ': selected file invalidated' + elif sw2 == 0x84: status += ': FCI not formatted' + elif sw2 == 0x85: status += ': selected file in termination state' + elif sw2 == 0x86: status += ': no input data available ' \ + 'from a sensor on the card' + elif 0x01 < sw2 < 0x81: status += ': card has %s bytes pending' \ + % toHexString([sw2])[1] + else: status += ': undefined SW2 code: 0x%s' % toHexString([sw2]) + elif sw1 == 0x63: + status = 'warning processing: state of non-volatile memory changed' + if sw2 == 0x00: status += ': no information given' + elif sw2 == 0x81: status += ': file filled up by the last write' + elif 0xC0 <= sw2 <= 0xCF: status += ': counter provided by %s' \ + % toHexString([sw2])[1] + else: status += ': undefined SW2 code: 0x%s' % toHexString([sw2]) + elif sw1 == 0x64: + status = 'execution error: state of non-volatile memory unchanged' + if sw2 == 0x01: status += ': immediate response expected ' \ + 'by the card' + elif 0x01 < sw2 < 0x81: status += ': command aborted ' \ + 'by the card, recovery of %s bytes is needed' \ + % toHexString([sw2]) + else: status += ': undefined SW2 code: 0x%s' % toHexString([sw2]) + elif sw1 == 0x65: + status = 'execution error: state of non-volatile memory changed' + if sw2 == 0x00: status += ': no information given' + elif sw2 == 0x81: status += ': memory failure' + else: status += ': undefined SW2 code: 0x%s' % toHexString([sw2]) + elif sw1 == 0x66: status = 'execution error: reserved for ' \ + 'security-related issues' + elif sw1 == 0x67 and sw2 == 0x00: status = 'checking error: ' \ + 'wrong length (P3 parameter)' + elif sw1 == 0x68: + status = 'checking error: functions in CLA not supported' + if sw2 == 0x00: status += ': no information given' + elif sw2 == 0x81: status += ': logical channel not supported' + elif sw2 == 0x82: status += ': secure messaging not supported' + elif sw2 == 0x83: status += ': last command of the chain expected' + elif sw2 == 0x84: status += ': command chaining not supported' + else: status += ': undefined SW2 code: 0x%s' % toHexString([sw2]) + elif sw1 == 0x69: + status = 'checking error: command not allowed' + if sw2 == 0x00: status += ': no information given' + elif sw2 == 0x81: status += ': command incompatible with ' \ + 'file structure' + elif sw2 == 0x82: status += ': security status not satisfied' + elif sw2 == 0x83: status += ': authentication method blocked' + elif sw2 == 0x84: status += ': referenced data invalidated' + elif sw2 == 0x85: status += ': conditions of use not satisfied' + elif sw2 == 0x86: status += ': command not allowed (no current EF)' + elif sw2 == 0x87: status += ': expected SM data objects missing' + elif sw2 == 0x88: status += ': SM data objects incorrect' + else: status += ': undefined SW2 code: 0x%s' % toHexString([sw2]) + elif sw1 == 0x6A: + status = 'checking error: wrong parameter(s) P1-P2' + if sw2 == 0x00: status += ': no information given' + elif sw2 == 0x80: status += ': incorrect parameters ' \ + 'in the data field' + elif sw2 == 0x81: status += ': function not supported' + elif sw2 == 0x82: status += ': file not found' + elif sw2 == 0x83: status += ': record not found' + elif sw2 == 0x84: status += ': not enough memory space in the file' + elif sw2 == 0x85: status += ': Lc inconsistent with TLV structure' + elif sw2 == 0x86: status += ': incorrect parameters P1-P2' + elif sw2 == 0x87: status += ': Lc inconsistent with P1-P2' + elif sw2 == 0x88: status += ': referenced data not found' + elif sw2 == 0x89: status += ': file already exists' + elif sw2 == 0x8A: status += ': DF name already exists' + else: status += ': undefined SW2 code: 0x%s' % toHexString([sw2]) + elif sw1 == 0x6B and sw2 == 0x00: status = 'checking error: '\ + 'wrong parameter(s) P1-P2' + elif sw1 == 0x6C: status = 'checking error: wrong length Le: ' \ + 'exact length is %s' % toHexString([sw2]) + elif sw1 == 0x6D and sw2 == 0x00: status = 'checking error: ' \ + 'instruction code not supported or invalid' + elif sw1 == 0x6E and sw2 == 0x00: status = 'checking error: ' \ + 'class not supported' + elif sw1 == 0x6F and sw2 == 0x00: status = 'checking error: ' \ + 'no precise diagnosis' + return status + + def sr_apdu(self, apdu, force=False): + ''' + sr_apdu(apdu=[0x.., 0x.., ...]) -> + list [ string(apdu sent information), + string(SW codes interpretation), + 2-tuple(sw1, sw2), + list(response bytes) ] + + generic function to send apdu, receive and interpret response + force: force card reconnection if pyscard transmission fails + ''' + if force: + try: + data, sw1, sw2 = self.cardservice.connection.transmit(apdu) + except CardConnectionException: + ISO7816.__init__(self, CLA = self.CLA) + data, sw1, sw2 = self.cardservice.connection.transmit(apdu) + else: + data, sw1, sw2 = self.cardservice.connection.transmit(apdu) + # replaces INS code by strings when available + if apdu[1] in self.INS_dic.keys(): + apdu_name = self.INS_dic[apdu[1]] + ' ' + else: + apdu_name = '' + sw_stat = self.sw_status(sw1, sw2) + return ['%sapdu: %s' % (apdu_name, toHexString(apdu)), + 'sw1, sw2: %s - %s' % ( toHexString([sw1, sw2]), sw_stat ), + (sw1, sw2), + data ] + + def bf_cla(self, start=0, param=[0xA4, 0x00, 0x00, 0x02, 0x3F, 0x00]): + ''' + bf_cla( start=int(starting CLA), + param=list(bytes for selecting file 0x3F, 0x00) ) -> + list( CLA which could be supported ) + + tries all classes CLA codes to check the possibly supported ones + prints CLA suspected to be supported + returns the list of those CLA codes + + WARNING: + can block the card definitively + Do not do it with your own VISA / MASTERCARD + ''' + clist = [] + for i in range(start, 256): + ret = self.sr_apdu([i] + param) + if ret[2] != (0x6E, 0x00): + # DBG log + log(3, '(CLA bruteforce) %s' % ret) + clist.append(i) + return clist + + def bf_ins(self, start=0): + ''' + bf_cla( start=int(starting INS) ) + -> list( INS which could be supported ) + + tries all instructions INS codes to check the supported ones + prints INS suspected to be supported + returns the list of those INS codes + + WARNING: + can block the card definitively + Do not do it with your own VISA / MASTERCARD + ''' + ilist = [] + for i in range(start, 256): + if self.dbg >= 3: + log(3, '(INS bruteforce) testing %d for INS code ' \ + 'with %d CLA code' % (i, self.CLA)) + ret = self.sr_apdu([self.CLA, i, 0x00, 0x00]) + if ret[2] != (0x6D, 0x00): + # DBG log + log(3, '(INS bruteforce) %s' % ret) + ilist.append(i) + return ilist + + ### + # Below is defined a list of standard commands to be used with (U)SIM cards + # They are mainly defined and described in + # ISO 7816 and described further in ETSI 101.221 + ### + def READ_BINARY(self, P1=0x00, P2=0x00, Le=0x01): + ''' + APDU command to read the content of EF file with transparent structure + Le: length of data bytes to be read + + call sr_apdu method + ''' + READ_BINARY = [self.CLA, 0xB0, P1, P2, Le] + return self.sr_apdu(READ_BINARY) + + def WRITE_BINARY(self, P1=0x00, P2=0x00, Data=[]): + ''' + APDU command to write the content of EF file with transparent structure + + Data: list of data bytes to be written + call sr_apdu method + ''' + WRITE_BINARY = [self.CLA, 0xD0, P1, P2, len(Data)] + Data + return self.sr_apdu(WRITE_BINARY) + + def UPDATE_BINARY(self, P1=0x00, P2=0x00, Data=[]): + ''' + APDU command to update the content of EF file with transparent structure + + Data: list of data bytes to be written + call sr_apdu method + ''' + UPDATE_BINARY = [self.CLA, 0xD6, P1, P2, len(Data)] + Data + return self.sr_apdu(UPDATE_BINARY) + + def ERASE_BINARY(self, P1=0x00, P2=0x00, Lc=None, Data=[]): + ''' + APDU command to erase the content of EF file with transparent structure + + Lc: 'None' or '0x02' + Data: list of data bytes to be written + call sr_apdu method + ''' + if Lc is None: + ERASE_BINARY = [self.CLA, 0x0E, P1, P2] + else: + ERASE_BINARY = [self.CLA, 0x0E, P1, P2, 0x02] + Data + return self.sr_apdu(ERASE_BINARY) + + def READ_RECORD(self, P1=0x00, P2=0x00, Le=0x00): + ''' + APDU command to read the content of EF file with record structure + + P1: record number + P2: reference control + Le: length of data bytes to be read + call sr_apdu method + ''' + READ_RECORD = [self.CLA, 0xB2, P1, P2, Le] + return self.sr_apdu(READ_RECORD) + + def WRITE_RECORD(self, P1=0x00, P2=0x00, Data=[]): + ''' + APDU command to write the content of EF file with record structure + + P1: record number + P2: reference control + Data: list of data bytes to be written in the record + call sr_apdu method + ''' + WRITE_RECORD = [self.CLA, 0xD2, P1, P2, len(Data)] + Data + return self.sr_apdu(WRITE_RECORD) + + def APPEND_RECORD(self, P2=0x00, Data=[]): + ''' + APDU command to append a record on EF file with record structure + + P2: reference control + Data: list of data bytes to be appended on the record + call sr_apdu method + ''' + APPEND_RECORD = [self.CLA, 0xE2, 0x00, P2, len(Data)] + Data + return self.sr_apdu(APPEND_RECORD) + + def UPDATE_RECORD(self, P1=0x00, P2=0x00, Data=[]): + ''' + APDU command to update the content of EF file with record structure + + P1: record number + P2: reference control + Data: list of data bytes to update the record + call sr_apdu method + ''' + APPEND_RECORD = [self.CLA, 0xDC, P1, P2, len(Data)] + Data + return self.sr_apdu(APPEND_RECORD) + + def GET_DATA(self, P1=0x00, P2=0x00, Le=0x01): + ''' + APDU command to retrieve data object + + P1 and P2: reference control for data object description + Le: number of bytes expected in the response + call sr_apdu method + ''' + GET_DATA = [self.CLA, 0xCA, P1, P2, Le] + return self.sr_apdu(GET_DATA) + + def PUT_DATA(self, P1=0x00, P2=0x00, Data=[]): + ''' + APDU command to store data object + + P1 and P2: reference control for data object description + Data: list of data bytes to put in the data object structure + call sr_apdu method + ''' + if len(Data) == 0: + PUT_DATA = [self.CLA, 0xDA, P1, P2] + elif 1 <= len(Data) <= 255: + PUT_DATA = [self.CLA, 0xDA, P1, P2, len(Data)] + Data + # should never be the case, however... who wants to try + else: + PUT_DATA = [self.CLA, 0xDA, P1, P2, 0xFF] + Data[0:255] + return self.sr_apdu(PUT_DATA) + + def SELECT_FILE(self, P1=0x00, P2=0x00, Data=[0x3F, 0x00], \ + with_length=True): + ''' + APDU command to select file + + P1 and P2: selection control + Data: list of bytes describing the file identifier or address + call sr_apdu method + ''' + if with_length: + Data = [min(len(Data), 255)] + Data + SELECT_FILE = [self.CLA, 0xA4, P1, P2] + Data + return self.sr_apdu(SELECT_FILE) + + def VERIFY(self, P2=0x00, Data=[]): + ''' + APDU command to verify user PIN, password or security codes + + P2: reference control + Data: list of bytes to be verified by the card + call sr_apdu method + ''' + if len(Data) == 0: + VERIFY = [self.CLA, 0x20, 0x00, P2] + elif 1 <= len(Data) <= 255: + VERIFY = [self.CLA, 0x20, 0x00, P2, len(Data)] + Data + # should never be the case, however... who wants to try + else: + VERIFY = [self.CLA, 0x20, 0x00, P2, 0xFF] + Data[0:255] + return self.sr_apdu(VERIFY) + + def INTERNAL_AUTHENTICATE(self, P1=0x00, P2=0x00, Data=[]): + ''' + APDU command to run internal authentication algorithm + + P1 and P2: reference control (algo, secret key selection...) + Data: list of bytes containing the authentication challenge + call sr_apdu method + ''' + INTERNAL_AUTHENTICATE = [self.CLA, 0x88, P1, P2, len(Data)] + Data + return self.sr_apdu(INTERNAL_AUTHENTICATE) + + def EXTERNAL_AUTHENTICATE(self, P1=0x00, P2=0x00, Data=[]): + ''' + APDU command to conditionally update the security status of the card + after getting a challenge from it + + P1 and P2: reference control (algo, secret key selection...) + Data: list of bytes containing the challenge response + call sr_apdu method + ''' + if len(Data) == 0: + EXTERNAL_AUTHENTICATE = [self.CLA, 0x82, P1, P2] + elif 1 <= len(Data) <= 255: + EXTERNAL_AUTHENTICATE = [self.CLA, 0x82, P1, P2, len(Data)] + Data + # should never be the case, however... who wants to try + else: + EXTERNAL_AUTHENTICATE = [self.CLA, 0x82, P1, P2, 0xFF] + Data[0:255] + return self.sr_apdu(EXTERNAL_AUTHENTICATE) + + def GET_CHALLENGE(self): + ''' + APDU command to get a challenge for external entity authentication + to the card + + call sr_apdu method + ''' + GET_CHALLENGE = [self.CLA, 0x84, 0x00, 0x00] + return self.sr_apdu(GET_CHALLENGE) + + def MANAGE_CHANNEL(self, P1=0x00, P2=0x00): + ''' + APDU to open and close supplementary logical channels + + P1=0x00 to open, 0x80 to close + P2=0x00, 1, 2 or 3 to ask for logical channel number + call sr_apdu method + ''' + if (P1, P2) == (0x00, 0x00): + MANAGE_CHANNEL = [self.CLA, 0x70, P1, P2, 0x01] + else: + MANAGE_CHANNEL = [self.CLA, 0x70, P1, P2] + return self.sr_apdu(MANAGE_CHANNEL) + + def GET_RESPONSE(self, Le=0x01): + ''' + APDU command to retrieve data after selection + or other kind of request that should get an extensive reply + + Le: expected length of data + call sr_apdu method + ''' + GET_RESPONSE = [self.CLA, 0xC0, 0x00, 0x00, Le] + return self.sr_apdu(GET_RESPONSE) + + def ENVELOPE(self, Data=[]): + ''' + APDU command to encapsulate data (APDU or other...) + check ETSI TS 102.221 for some examples... + + Data: list of bytes + call sr_apdu method + ''' + if len(Data) == 0: + ENVELOPE = [self.CLA, 0xC2, 0x00, 0x00] + elif 1 <= len(Data) <= 255: + ENVELOPE = [self.CLA, 0xC2, 0x00, 0x00, len(Data)] + Data + return self.sr_apdu(ENVELOPE) + + def SEARCH_RECORD(self, P1=0x00, P2=0x00, Data=[]): + ''' + APDU command to seach pattern in the current EF file + with record structure + + P1: record number + P2: type of search + Data: list of bytes describing a pattern to search for + call sr_apdu method + ''' + SEARCH_RECORD = [self.CLA, 0xA2, P1, P2, len(Data)] + Data + return self.sr_apdu(SEARCH_RECORD) + + def DISABLE_CHV(self, P1=0x00, P2=0x00, Data=[]): + ''' + APDU command to disable CHV verification (such as PIN or password...) + + P1: let to 0x00... or read ISO and ETSI specifications + P2: type of CHV to disable + Data: list of bytes for CHV value + call sr_apdu method + ''' + DISABLE_CHV = [self.CLA, 0x26, P1, P2, len(Data)] + Data + return self.sr_apdu(DISABLE_CHV) + + def ENABLE_CHV(self, P1=0x00, P2=0x00, Data=[]): + ''' + APDU command to enable CHV verification (such as PIN or password...) + + P1: let to 0x00... or read ISO and ETSI specifications + P2: type of CHV to enable + Data: list of bytes for CHV value + call sr_apdu method + ''' + ENABLE_CHV = [self.CLA, 0x28, P1, P2, len(Data)] + Data + return self.sr_apdu(ENABLE_CHV) + + def UNBLOCK_CHV(self, P2=0x00, Data=[]): + ''' + APDU command to unblock CHV code (e.g. with PUK for deblocking PIN) + + P2: type of CHV to unblock + Lc: Empty or 0x10 + Data: if Lc=0x10, UNBLOCK_CHV (PUK) value and new CHV (PIN) values, + each are 8 digits + call sr_apdu method + ''' + if len(Data) != 16: + UNBLOCK_CHV = [self.CLA, 0x2C, 0x00, P2] + else: + UNBLOCK_CHV = [self.CLA, 0x2C, 0x00, P2, 0x10] + Data + return self.sr_apdu(UNBLOCK_CHV) + + ########################## + # evolved "macro" method for ISO7816 card + # need the "coms" attribute being an apdu_stack() + ########################## + def parse_file(self, Data=[]): + ''' + parse_file(self, Data) -> Dict() + + parses a list of bytes returned when selecting a file + interprets the content of some informative bytes + for file structure and parsing method... + ''' + ber = BERTLV_parser( Data ) + if self.dbg >= 3: + log(3, 'BER structure:\n%s' % ber) + if len(ber) > 1: + # TODO: implements recursive BER object parsing + log(1, '(parse_file) contain more than 1 BER object: '\ + '%s\nnot implemented' % ber) + + # for FCP control structure, precise parsing is done + # this structure seems to be the most used for (U)SIM cards + if ber[0][0][2] == 0x2: + fil = self.parse_FCP( ber[0][2] ) + fil['Control'] = 'FCP' + return fil + + # for FCI control structure, also trying to parse precisely + # this structure is used mainly in EMV cards + elif ber[0][0][2] == 0x10: + fil = self.parse_FCI( ber[0][2] ) + fil['Control'] = 'FCI' + return fil + + # for other control structure, DIY + fil = {} + if ber[0][0][2] == 0x4: + fil['Control'] = 'FMD' + if self.dbg: + log(1, '(parse_file) FMD file structure parsing ' \ + 'not implemented') + elif ber[0][0][2] == 0xF: + fil['Control'] = 'FCI' + if self.dbg: + log(1, '(parse_file) FCI 0xF file structure parsing ' + 'not implemented') + else: + fil['Control'] = ber[0][0] + if self.dbg: + log(1, '(parse_file) unknown file structure') + fil['Data'] = ber[0][2] + + return fil + + def parse_FCP(self, Data=[]): + ''' + parse_FCP(Data) -> Dict() + + parses a list of bytes returned when selecting a file + interprets the content of some informative bytes + for file structure and parsing method... + ''' + fil = {} + # loop on the Data bytes to parse TLV'style attributes + toProcess = Data + while len(toProcess) > 0: + # TODO: seemd full compliancy + # would require to work with the BERTLV parser... + [T, L, V] = first_TLV_parser(toProcess) + if self.dbg >= 3: + if T in self.file_tags.keys(): + Tag = self.file_tags[T] + else: + Tag = T + log(3, '(parse_FCP) Tag value %s / type %s: %s' % (T, Tag, V)) + + # do extra processing here + # File ID, DF name, Short file id + if T in (0x83, 0x84, 0x88): + fil[self.file_tags[T]] = V + # Security Attributes compact format + elif T == 0x8C: + fil[self.file_tags[T]] = V + fil = self.parse_compact_security_attribute(V, fil) + # Security Attributes ref to expanded + elif T == 0x8B: + fil[self.file_tags[T]] = V + fil = self.parse_expanded_security_attribute(V, fil) + # other security attributes... not implemented + elif T in (0x86, 0x8E, 0xA0, 0xA1, 0xAB): + fil[self.file_tags[T]] = V + # TODO: no concrete parsing at this time... + if self.dbg: + log(2, '(parse_FCP) parsing security attributes not ' \ + 'implemented for tag 0x%X' % T) + fil = self.parse_security_attribute(V, fil) + # file size or length + elif T in (0x80, 0x81): + fil[self.file_tags[T]] = sum( [ V[i] * pow(0x100, len(V)-i-1) \ + for i in range(len(V)) ] ) + # file descriptor, deducting file access, type and structure + elif T == 0x82: + assert( L in (2, 5) ) + fil[self.file_tags[T]] = V + fil = self.parse_file_descriptor(V, fil) + # life cycle status + elif T == 0x8A: + fil = self.parse_life_cycle(V, fil) + # proprietary information + elif T == 0xA5: + fil = self.parse_proprietary(V, fil) + else: + if T in self.file_tags.keys(): + fil[self.file_tags[T]] = V + else: + fil[T] = V + + # truncate the data to process and loop + if L < 256: + toProcess = toProcess[L+2:] + else: + toProcess = toProcess[L+4:] + + # and return the file + return fil + + @staticmethod + def parse_life_cycle(Data, fil): + ''' + parses a list of bytes provided in Data + interprets the content as the life cycle + and enriches the file dictionnary passed as argument + ''' + if Data[0] == 1: fil['Life Cycle Status'] = 'creation state' + elif Data[0] == 3: fil['Life Cycle Status'] = 'initialization state' + elif Data[0] in (5, 7): fil['Life Cycle Status'] = 'operational state' \ + ' - activated' + elif Data[0] in (4, 6): fil['Life Cycle Status'] = 'operational state' \ + ' - deactivated' + elif Data[0] in range(12, 15): fil['Life Cycle Status'] = \ + 'termination state' + elif Data[0] >= 16: fil['Life Cycle Status'] = 'proprietary' + else: fil['Life Cycle Status'] = 'RFU' + return fil + + @staticmethod + def parse_file_descriptor(Data, fil): + ''' + parses a list of bytes provided in Data + interprets the content as the file descriptor + and enriches the file dictionnary passed as argument + ''' + # parse the File Descriptor Byte + fd = Data[0] + fd_type = (fd >> 3) & 0b00111 + fd_struct = fd & 0b00000111 + # get Structure, Access and Type + # bit b8 + if (fd >> 7) & 0b1: fil['Structure'] = 'RFU' + # access bit b7 + if (fd >> 6) & 0b1: fil['Access'] = 'shareable' + else : fil['Access'] = 'not shareable' + # structure bits b1 to b3 + if fd_struct == 0: fil['Structure'] = 'no information' + elif fd_struct == 1: fil['Structure'] = 'transparent' + elif fd_struct == 2: fil['Structure'] = 'linear fixed' + elif fd_struct == 3: fil['Structure'] = 'linear fixed TLV' + elif fd_struct == 4: fil['Structure'] = 'linear variable' + elif fd_struct == 5: fil['Structure'] = 'linear variable TLV' + elif fd_struct == 6: fil['Structure'] = 'cyclic' + elif fd_struct == 7: fil['Structure'] = 'cyclic TLV' + else : fil['Structure'] = 'RFU' + # type bits b4 to b6 + if fd_type == 0: fil['Type'] = 'EF working' + elif fd_type == 1: fil['Type'] = 'EF internal' + elif fd_type == 7: + fil['Type'] = 'DF' + if fd_struct == 1: fil['Structure'] = 'BER-TLV' + elif fd_struct == 2: fil['Structure'] = 'TLV' + else: fil['Type'] = 'EF proprietary' + + # for linear and cyclic EF: + # the following is convenient for UICC, + # but looks not fully conform to ISO standard + # see coding convention in ISO 7816-4 Table 87 + if len(Data) == 5: + fil['Record Length'], fil['Record Number'] = Data[3], Data[4] + + return fil + + @staticmethod + def parse_proprietary(Data, fil): + ''' + parses a list of bytes provided in Data + interprets the content as the proprietary parameters + and enriches the file dictionnary passed as argument + ''' + propr_tags = { + 0x80:"UICC characteristics", + 0x81:"Application power consumption", + 0x82:"Minimum application clock frequency", + 0x83:"Amount of available memory", + 0x84:"File details", + 0x85:"Reserved file size", + 0x86:"Maximum file size", + 0x87:"Supported system commands", + 0x88:"Specific UICC environmental conditions", + } + while len(Data) > 0: + [T, L, V] = first_TLV_parser( Data ) + if T in propr_tags.keys(): + fil[propr_tags[T]] = V + Data = Data[L+2:] + return fil + + @staticmethod + def parse_compact_security_attribute(Data, fil): + ''' + parses a list of bytes provided in Data + interprets the content as the compact form for security parameters + and enriches the file dictionnary passed as argument + ''' + # See ISO-IEC 7816-4 section 5.4.3, with compact and expanded format + AM = Data[0] + SC = Data[1:] + sec = '#' + + # check access mode + if 'Type' in fil.keys(): + # DF security attributes parsing + if fil['Type'] == 'DF': + sec += self._DF_access_mode(AM) + # EF security attributes parsing + else: + sec += self._EF_access_mode(AM) + # loop on security conditions for the given access mode: + for cond in SC: + sec += self._sec_cond(cond) + # return security conditions if parsed, return raw bytes otherwise + if sec == '#': + fil['Security Attributes raw'] = Data + else: + fil['Security Attributes'] = sec + return fil + + @staticmethod + def _DF_access_mode(AM): + sec = '' + if AM & 0b10000000 == 0: + if AM & 0b01000000: sec += ' DELETE FILE #' + if AM & 0b00100000: sec += ' TERMINATE DF #' + if AM & 0b00010000: sec += ' ACTIVATE FILE #' + if AM & 0b00001000: sec += ' DEACTIVATE FILE #' + if AM & 0b00000100: sec += ' CREATE DF #' + if AM & 0b00000010: sec += ' CREATE EF #' + if AM & 0b00000001: sec += ' DELETE FILE #' + return sec + + @staticmethod + def _EF_access_mode(AM): + sec = '' + if AM & 0b10000000 == 0: + if AM & 0b01000000: sec += ' DELETE FILE #' + if AM & 0b00100000: sec += ' TERMINATE EF #' + if AM & 0b00010000: sec += ' ACTIVATE FILE #' + if AM & 0b00001000: sec += ' DEACTIVATE FILE #' + if AM & 0b00000100: sec += ' WRITE / APPEND #' + if AM & 0b00000010: sec += ' UPDATE / ERASE #' + if AM & 0b00000001: sec += ' READ / SEARCH #' + return sec + + @staticmethod + def _Obj_access_mode(AM): + sec = '' + if AM & 0b00000100: sec += 'MANAGE SEC ENVIRONMENT #' + if AM & 0b00000010: sec += 'PUT DATA' + if AM & 0b00000001: sec += 'GET DATA' + return sec + + @staticmethod + def _Tab_access_mode(val): + sec = '' + if AM & 0b10000000 == 0: + if AM & 0b01000000: sec += ' CREATE / DELETE USER #' + if AM & 0b00100000: sec += ' GRANT / REVOKE #' + if AM & 0b00010000: sec += ' CREATE TABLE / VIEW / DICT #' + if AM & 0b00001000: sec += ' DROP TABLE / VIEW #' + if AM & 0b00000100: sec += ' INSERT #' + if AM & 0b00000010: sec += ' UPDATE / DELETE #' + if AM & 0b00000001: sec += ' FETCH #' + return sec + + @staticmethod + def _sec_cond(cond): + sec = '' + if cond == 0 : sec += ' Always #' + elif cond == 0xff: sec += ' Never #' + else: + sec += ' SEID %s #' % (cond & 0b00001111) + if cond & 0b10000000: sec += ' all conditions #' + else: sec += ' at least 1 condition #' + if cond & 0b01000000: sec += ' secure messaging #' + if cond & 0b00100000: sec += ' external authentication #' + if cond & 0b00010000: sec += ' user authentication #' + return sec + + @staticmethod + def parse_expanded_security_attribute(Data, fil): + ''' + check references to EF_ARR file containing access conditions + see ISO 7816-4 + ''' + # self.ARR = {ARR_id:[ARR_content],...} + return fil + file_length = len(Data) + if file_length == 1: + ARR_byte = Data + elif file_length == 3: + ARR_ref = Data[0:2] + ARR_byte = Data[2:3] + elif file_length > 3: + ARR_ref = Data[0:2] + # handle SEID and ARR.byte in 2 // lists + SEID_bytes, ARR_bytes = [], [] + # in case file_length is not even: truncate it... + if file_length%2 == 1: file_length -= 1 + # parse SEID / ARR.bytes + for i in range(2, file_length, 2): + SEID_byte.append(Data[i:i+1]) + ARR_byte.append(Data[:+1:i+2]) + + + @staticmethod + def parse_security_attribute(Data, fil): + ''' + TODO: to implement... + + need to work further on how to do it (with ref to EF_ARR) + ''' + # See ISO-IEC 7816-4 section 5.4.3, with compact and expanded format + # not implemented yet (looks like useless for (U)SIM card ?) + return fil + + def parse_FCI(self, Data=[]): + ''' + parse_FCI(Data) -> Dict() + + parses a list of bytes returned when selecting a file + interprets the content of some informative bytes + for file structure and parsing method... + ''' + fil = {} + # loop on the Data bytes to parse TLV'style attributes + toProcess = Data + while len(toProcess) > 0: + # TODO: seemd full compliancy + # would require to work with the BERTLV parser... + [T, L, V] = first_TLV_parser(toProcess) + if self.dbg >= 3: + if T in self.file_tags.keys(): + Tag = self.file_tags[T] + else: + Tag = T + log(3, '(parse_FCI) Tag value %s / type %s: %s' % (T, Tag, V)) + + # application template + if T == 0x61: + fil['Application Template'] = V + + + + # do extra processing here + # File ID, DF name, Short file id + elif T in (0x83, 0x84, 0x88): + fil[self.file_tags[T]] = V + # Security Attributes compact format + elif T == 0x8C: + fil[self.file_tags[T]] = V + fil = self.parse_compact_security_attribute(V, fil) + # Security Attributes ref to expanded + elif T == 0x8B: + fil[self.file_tags[T]] = V + fil = self.parse_expanded_security_attribute(V, fil) + # other security attributes... not implemented + elif T in (0x86, 0x8E, 0xA0, 0xA1, 0xAB): + fil[self.file_tags[T]] = V + # TODO: no concrete parsing at this time... + if self.dbg: + log(2, '(parse_FCP) parsing security attributes not ' \ + 'implemented for tag 0x%X' % T) + fil = self.parse_security_attribute(V, fil) + # file size or length + elif T in (0x80, 0x81): + fil[self.file_tags[T]] = sum( [ V[i] * pow(0x100, len(V)-i-1) \ + for i in range(len(V)) ] ) + # file descriptor, deducting file access, type and structure + elif T == 0x82: + assert( L in (2, 5) ) + fil[self.file_tags[T]] = V + fil = self.parse_file_descriptor(V, fil) + # life cycle status + elif T == 0x8A: + fil = self.parse_life_cycle(V, fil) + # proprietary information + elif T == 0xA5: + fil = self.parse_proprietary(V, fil) + else: + if T in self.file_tags.keys(): + fil[self.file_tags[T]] = V + else: + fil[T] = V + + # truncate the data to process and loop + if L < 256: + toProcess = toProcess[L+2:] + else: + toProcess = toProcess[L+4:] + + # and return the file + return fil + + + def read_EF(self, fil): + ''' + interprets the content of file parameters (Structure, Size, Length...) + and enriches the file dictionnary passed as argument + with "Data" key and corresponding + - list of bytes for EF transparent + - list of list of bytes for cyclic or linear EF + ''' + # read EF transparent data + if fil['Structure'] == 'transparent': + self.coms.push( self.READ_BINARY(Le=fil['Size']) ) + if self.coms()[2] != (0x90, 0x00): + if self.dbg >= 3: + log(3, '(read_EF) %s' % self.coms()) + return fil + fil['Data'] = self.coms()[3] + + # read EF cyclic / linear all records data + elif fil['Structure'] != 'transparent': + fil['Data'] = [] + # for record data: need to check the number of recordings + # stored in the file, and iterate for each + for i in range( (fil['Size'] / fil['Record Length']) ): + self.coms.push( self.READ_RECORD(P1=i+1, P2=0x04, \ + Le=fil['Record Length']) ) + if self.coms()[2] != (0x90, 0x00): + # should mean there is an issue + # somewhere in the file parsing process + if self.dbg >= 2: + log(2, '(read_EF) error in iterating the RECORD ' \ + 'parsing at iteration %s\n%s' % (i, self.coms())) + return fil + if self.coms()[3][1:] == len(self.coms()[3][1:]) * [255]: + # record is empty, contains padding only + pass + else: + fil['Data'].append(self.coms()[3]) + + # return the [Data] for transparent or + # [[Record1],[Record2]...] for cyclic / linear + return fil + + def select(self, addr=[0x3F, 0x00], type="fid", with_length=True): + ''' + self.select(addr=[0x.., 0x..], type="fid", with_length=True) + -> dict() on success, None on error + + selects the file at the given address + if error, returns None + if processing correct: gets response with info on the file + if EF file: tries to read the data within the file + security conditions, aka PIN/ADM codes, need to be satified + returns the complete file structure and content as a dictionnary + `self`.parse_file() method currently implements only FCP structure + for working with USIM + + different types of file selection are possible (P1 parameter of the + SELECT_FILE APDU): + "fid": select by file id, only the direct child of current DF + or parent DF or immediate children of parent DF + current DF: last selected MF / DF / ADF + "pmf": select by path from MF + "pdf": select by path from last selected MF / DF / ADF + (or relative path) + "aid": select by ADF (Application) name + + with_length: correspond to the Lc byte preprended to the address + in the SELECT_FILE APDU + + APDUs exchanged available thanks to the attribute `self`.coms + ''' + # get the UICC trigger + is_UICC = isinstance(self, UICC) + + # handle type of selection: + if type == "pmf": P1 = 0x08 + elif type == "pdf": P1 = 0x09 + elif type == "aid": P1 = 0x04 + # the default case, selection by "fid": + else: P1 = 0x00 + + # for UICC instance + # ask the return of the FCP template for the selected file: + if is_UICC: + P2 = 0x04 + else: + P2 = 0x00 + + # used to get back to MF without getting MF attributes: + #if len(addr) == 0: + # P1, P2 = 0x00, 0x0C + # this is however not correct... commented + + # select file and check SW; if error, returns None, + # else get response + self.coms.push(self.SELECT_FILE(P1=P1, P2=P2, Data=addr, \ + with_length=with_length)) + + # different SW codes for UICC and old ISO card (e.g. SIM) + if is_UICC and self.coms()[2][0] != 0x61 \ + or not is_UICC and self.coms()[2][0] != 0x9F: + if self.dbg >= 3: + log(3, '(select) %s' % self.coms()) + return None + + # get response and check SW: + # if error, return None, else parse file info + self.coms.push(self.GET_RESPONSE(Le=self.coms()[2][1])) + if self.coms()[2] != (0x90, 0x00): + if self.dbg >= 3: + log(3, '(select) %s' % self.coms()) + return None + + data = self.coms()[3] + # take the parse_file() method from the instance: + # ISO7816, UICC (for USIM) or SIM + file = self.parse_file(data) + if file['Type'][0:2] == 'EF': + file = self.read_EF(file) + + # finally returns the whole file dictionnary, + # containing the ['Data'] key for EF file content + return file + + ############### + # The following may need some improvements + ############### + + def go_to_path(self, path=[], under_AID=None): + ''' + self.go_to_path(path=[0x.., 0x.., 0x.., 0x.., ..], under_AID=None) + -> void + + selects all DF addresses successively from the path given + uses the .select() method with "fid" as selection type + works with AID number too + ''' + # check path length + if len(path) % 2: + log(1, '(go_to_path) path length not correct: %s' % path) + return + # init under MF + self.select([0x3F, 0x00]) + # init under AID if needed + if isinstance(self, UICC) and under_AID is not None: + self.select_by_aid(under_AID) + # select over the whole path + [self.select(addr, 'fid') for addr in \ + [path[i:i+2] for i in range(0,len(path),2)]] + + + # the MF or AID directory structure is a dictionnary: + # e.g. + #self._MF_struct = { + #tuple(df_absolute_addr) : (child_df1, child_df2, ...), + #...} + # or + #self._AID1_struct ... + # + # this helps to build the blacklist: + + def make_blacklist(self, DF_path=[], under_AID=None): + ''' + self.make_blacklist(DF_path=[0x.., 0x.., 0x.., 0x..], under_AID=None) + -> list( DFs ) + + check dictionnaries describing MF or AID directory structure + and return DF not to select when scanning for file ID under a DF + ''' + # IC card Master File, never reselect it... + MF = [0x3F, 0x00] + # you should also avoid to reselect it + # looks like an alias of the MF + pseudo_MF = [0x3F, 0xFF] + # init BlackList with MF and current DF + BL = [ MF, pseudo_MF ] + # + # check if current DF is root: returns directly + current_DF = DF_path[-2:] + # then, AID directory structure to use if under_AID + if under_AID: + if not hasattr(self, '_AID%i_struct' % under_AID): + if self.dbg >= 2: + log(2, '(make_blacklist) AID%i directory structure not' \ + ' found' % under_AID) + if current_DF: BL.append(current_DF) + return BL + dir_struct = getattr(self, '_AID%i_struct' % under_AID) + # else, select MF directory structure to use + else: + if not hasattr(self, '_MF_struct'): + if self.dbg >= 2: + log(2, '(make_blacklist) MF directory structure not found') + if current_DF: BL.append(current_DF) + return BL + dir_struct = self._MF_struct + # + # if parent_DF is root (MF or AID), add only childs of root + # which contains the current DF + if len(DF_path) == 2: + BL.extend( dir_struct[()] ) + return BL + # if DF is 2nd order or more, add father DF and its children + # one of which contains the current DF + elif len(DF_path) >= 4: + BL.append( DF_path[-4:-2] ) + BL.extend( dir_struct[tuple(DF_path[:-2])] ) + return BL + # if DF_path is empty or malformed... + else: + if current_DF: BL.append(current_DF) + return BL + + def scan_DF(self, dir_path=[], under_AID=None, \ + hi_addr=(0, 0xff), lo_addr=(0, 0xff)): + ''' + self.scan_DF(dir_path=[0x.., 0x.., 0x.., 0x..], under_AID=None) + -> list(filesystem), list(child_DF) + + try to select all file addresses under a given DF path + hi_addr: 8 MSB of the file address to brute force + lo_addr: 8 LSB of the file address to brute force + avoid selecting blacklisted files (MF, parent_DF, brother_DF, current_DF) + return list of all found files (EF, DF) and list of child DF + ''' + # build blacklist of addresses from the current directory structure + # and selected path, in order to select only child file ID: + BL = self.make_blacklist(dir_path, under_AID) + if self.dbg >= 2: + log(3, 'blacklist: %s' % BL) + # init variables to return + FS, child_DF = [], [] + # + # init to path + self.go_to_path(dir_path, under_AID) + # bruteforce child file addresses + i, j = 0, 0 + for i in range(hi_addr[0], hi_addr[1]+1): + # just make it verbose... + if self.dbg and i%32 == 0: + log(3, '(scan_DF) addr: %s %s' % (dir_path, [i, j])) + for j in range(lo_addr[0], lo_addr[1]+1): + addr = [i, j] + # avoid selection of blacklisted addresses: + if addr in BL: + pass + # select by direct file id + else: + file = self.select(addr, 'fid') + if file: + if self.dbg: + log(3, '(scan_DF) found file at path: %s' \ + % (dir_path + addr)) + # keep track of absolute path + file['Absolut Path'] = dir_path + addr + # add result to grow the filesystem + FS.append(file) + # now fill in child_DF to potentially + # grow the directory structure + if 'Type' in file.keys() and file['Type'] == 'DF': + # for UICC, avoid reselecting AID DF + if under_AID and 'DF Name' in file.keys() \ + and file['DF Name'] == self.AID[under_AID-1]: + if self.dbg: + log(3, '(scan_DF) USIM AID alias at %s: ' \ + 'ignoring it' % addr) + else: + child_DF.append(addr) + # replace selection to parent_path + self.go_to_path(dir_path, under_AID) + # + # re-initialize at MF and return + self.select([0x3F, 0x00]) + return FS, child_DF + + def explore_DF(self, DF_path=[], under_AID=None, recursive=True): + ''' + self.explore_DF(dir_path=[0x.., 0x.., 0x.., 0x..], under_AID=None, \ + recursive=True) + -> None + + try to select all file addresses under a given DF path recursively with + scan_DF() method, possibly recursively (can be an `int`, to stop after + a certain level) + fill in self.FS dictionnary with found DF and files + and self._MF_struct or self._AID`num`_struct with directory structure + ''' + # init by scanning the given DF_path (MF or AID) + FS, child_DF = self.scan_DF(DF_path, under_AID) + # then init or extend self._MF_struct or + # self._AID`num`_struct for blacklist management + if under_AID: + # if _AID`num`_struct not initialized (we are at AID root): + if not hasattr(self, '_AID%i_struct' % under_AID): + setattr(self, '_AID%i_struct' % under_AID, {}) + # then populate _AID`num`_struct with found child_DF + getattr(self, '_AID%i_struct' % under_AID)[tuple(DF_path)] = child_DF + else: + # if _MF_struct not initialized (we are at MF root): + if not hasattr(self, '_MF_struct'): + self._MF_struct = {} + self._MF_struct[tuple(DF_path)] = child_DF + # populate the self.FS + if not hasattr(self, 'FS'): + self.init_FS() + self.FS.extend(FS) + # + # and loop to scan recursively over child_DF + if recursive: + # manage maximum recursion level: do not scan children DF + # if absolut path is over recursion level + if type(recursive) == int and len(DF_path)/2 >= recursive: + return + # scan children DF + for path in map(DF_path.__add__, child_DF): + print('recursive selection of path %s' % path) + self.explore_DF(path, under_AID, recursive) + + def init_FS(self): + self.FS = [] + + +############################################## +# UICC is defined in ETSI 102.221 mainly, +# and used for many telco applications +############################################## + +class UICC(ISO7816): + ''' + define attributes, methods and facilities for ETSI UICC card + check UICC specifications mainly in ETSI TS 102.221 + + inherits (eventually overrides) methods and objects from ISO7816 class + use self.dbg = 1 or more to print live debugging information + ''' + AID_RID = { + (0xA0, 0x00, 0x00, 0x00, 0x09): 'ETSI', + (0xA0, 0x00, 0x00, 0x00, 0x87): '3GPP', + (0xA0, 0x00, 0x00, 0x03, 0x43): '3GPP2', + (0xA0, 0x00, 0x00, 0x04, 0x12): 'OMA', + (0xA0, 0x00, 0x00, 0x04, 0x24): 'WiMAX', + } + AID_ETSI_app_code = { + (0x00, 0x00): 'Reserved', + (0x00, 0x01): 'GSM', + (0x00, 0x02): 'GSM SIM Toolkit', + (0x00, 0x03): 'GSM SIM API for JavaCard', + (0x00, 0x04): 'Tetra', + (0x00, 0x05): 'UICC API for JavaCard', + (0x01, 0x01): 'DVB CBMS KMS', + } + AID_3GPP_app_code = { + (0x10, 0x01): 'UICC', + (0x10, 0x02): 'USIM', + (0x10, 0x03): 'USIM Toolkit', + (0x10, 0x04): 'ISIM', + (0x10, 0x05): 'USIM API for JavaCard', + (0x10, 0x06): 'ISIM API for JavaCard', + (0x10, 0x05): 'Contact Manager API for JavaCard', + } + AID_3GPP2_app_code = { + (0x10, 0x02): 'CSIM', + } + AID_country_code = { + (0xFF, 0x33): 'France', + (0xFF, 0x44): 'United Kingdom', + (0xFF, 0x49): 'Germany', + } + + pin_status = { + 0x01 : "PIN Appl 1", + 0x02 : "PIN Appl 2", + 0x03 : "PIN Appl 3", + 0x04 : "PIN Appl 4", + 0x05 : "PIN Appl 5", + 0x06 : "PIN Appl 6", + 0x07 : "PIN Appl 7", + 0x08 : "PIN Appl 8", + 0x09 : "RFU", + 0x0A : "ADM1", + 0x0B : "ADM2", + 0x0C : "ADM3", + 0x0D : "ADM4", + 0x0E : "ADM5", + 0x11 : "PIN Universal PIN", + 0x81 : "Second PIN Appl 1", + 0x82 : "Second PIN Appl 2", + 0x83 : "Second PIN Appl 3", + 0x84 : "Second PIN Appl 4", + 0x85 : "Second PIN Appl 5", + 0x86 : "Second PIN Appl 6", + 0x87 : "Second PIN Appl 7", + 0x88 : "Second PIN Appl 8", + 0x89 : "RFU", + 0x8A : "ADM6", + 0x8B : "ADM7", + 0x8C : "ADM8", + 0x8D : "ADM9", + 0x8E : "ADM10", + } + + files = [ + ([0x3F, 0x00], 'MF', 'MF'), + ([0x2F, 0x00], 'EF', 'EF_DIR'), + ([0x2F, 0x01], 'EF', 'EF_ATR'), + ([0x2F, 0x05], 'EF', 'EF_PL'), + ([0x2F, 0x06], 'EF', 'EF_ARR'), + ([0x2F, 0x2E], 'EF', 'EF_ICCID'), + ([0x7F, 0xFF], 'DF', 'current ADF'), + ([0x7F, 0x10], 'DF', 'DF_TELECOM'), + ([0x7F, 0x10, 0x5F, 0x50], 'DF', 'DF_GRAPHICS'), + ([0x7F, 0x10, 0x5F, 0x3A], 'DF', 'DF_PHONEBOOK'), + ([0x7F, 0x20], 'DF', 'DF_GSM'), + ([0x7F, 0x21], 'DF', 'DF_DCS1800'), + ([0x7F, 0x22], 'DF', 'DF_IS-41'), + ([0x7F, 0x23], 'DF', 'DF_FP-CTS'), + ([0x7F, 0x24], 'DF', 'DF_TIA-EIA136'), + ([0x7F, 0x25], 'DF', 'DF_TIA-EIA95'), + ([0x7F, 0x80], 'DF', 'DF_PDC'), + ([0x7F, 0x90], 'DF', 'DF_TETRA'), + ([0x7F, 0x31], 'DF', 'DF_iDEN'), + ] + + def __init__(self): + ''' + initializes like an ISO7816-4 card with CLA=0x00 + initialized on the MF + ''' + ISO7816.__init__(self, CLA=0x00) + self.AID = [] + + if self.dbg >= 2: + log(3, '(UICC.__init__) type definition: %s' % type(self)) + log(3, '(UICC.__init__) CLA definition: %s' % hex(self.CLA)) + + def parse_file(self, Data=[]): + ''' + parse_file(Data=[0x12, 0x34, 0x56, 0x89]) -> dict(file) + mainly based on the ISO7816 parsing style + + parses a list of bytes returned when selecting a file + interprets the content of some informative bytes for right accesses, + type / format of file... see TS 102.221 + works over the UICC file structure (quite different from e.g. SIM card) + ''' + # First ISO7816 parsing + fil = ISO7816.parse_file(self, Data) + + # Then UICC extra attributes parsing + if 0xC6 in fil.keys(): + fil = self.parse_pin_status(fil[0xC6], fil) + del fil[0xC6] + + if 'File Identifier' in fil.keys(): + for ref in self.files: + if fil['File Identifier'] == ref[0]: + fil['Name'] = ref[2] + + # return the enriched file + return fil + + @staticmethod + def parse_pin_status(Data, fil): + ''' + parses a list of bytes provided in Data + interprets the content as the UICC pin status + and enriches the file dictionnary passed as argument + ''' + PS_DO = Data[2:2+Data[1]] + Data = Data[2+len(PS_DO):] + PIN_status = '' + while len(Data) > 0: + [T, L, V] = first_TLV_parser(Data) + assert( T in (0x83, 0x95) ) + if T == 0x95: # PIN usage + if (V[0] << 7) & 1: + PIN_status += '#use verification / encipherment ' \ + '/ external authentication: ' + elif (V[0] << 6) & 1: + PIN_status += '#use computation / decipherment ' \ + '/ internal authentication: ' + elif (V[0] << 5) & 1: + PIN_status += '#use SM response: ' + elif (V[0] << 4) & 1: + PIN_status += '#use SM command: ' + elif (V[0] << 3) & 1: + PIN_status += '#use PIN verification: ' + elif (V[0] << 3) & 1: + PIN_status += '#use biometric user verification: ' + elif V[0] == 0: + PIN_status += '#verification not required: ' + elif T == 0x83: # PIN status + if len(PIN_status) == 0: PIN_status = '#' + if 0x00 < V[0] < 0x12 or 0x81 <= V[0] < 0x90: + PIN_status += UICC.pin_status[V[0]] + '#' + elif 0x12 <= V[0] < 0x1E: + PIN_status += 'RFU (Global)#' + elif 0x90 <= V[0] < 0x9F: + PIN_status += 'RFU (Local)#' + else: + PIN_status += '#' + #if self.dbg >= 3: + # log(3, '(parse_pin_status) %s: %s; PIN status: %s' \ + # % (T, V, PIN_status)) + Data = Data[L+2:] + fil['PIN Status'] = PIN_status + return fil + + def get_AID(self): + ''' + checks EF_DIR at the MF level, + and available AID (Application ID) referenced + + puts it into self.AID + interprets and print the content of the self.AID list + ''' + #go back to MF and select EF_DIR + #self.select(addr=[]) + + # EF_DIR is at the MF level and contains Application ID: + EF_DIR = self.select([0x2F, 0x00], type='pmf') + if self.dbg >= 3: + log(3, '(get_AID) EF_DIR: %s' % EF_DIR) + if EF_DIR is None: + return + + # EF_DIR is an EF with linear fixed structure: contains records: + for rec in EF_DIR['Data']: + # check for a (new) AID: + if (rec[0], rec[2]) == (0x61, 0x4F) and len(rec) > 6 \ + and rec[4:4+rec[3]] not in self.AID: + self.AID.append( rec[4:4+rec[3]] ) + + #for aid in self.AID: + # self.interpret_AID(aid) + + @staticmethod + def interpret_AID(aid=[]): + ''' + interprets and prints the aid provided + ''' + if len(aid) < 11: + return + # check AID format + aid_rid = tuple(aid[0:5]) + aid_app = tuple(aid[5:7]) + aid_country = tuple(aid[7:9]) + aid_provider = tuple(aid[9:11]) + + # get AID application code, depending on SDO... + if aid_rid == (0xA0, 0x00, 0x00, 0x00, 0x09) \ + and aid_app in UICC.AID_ETSI_app_code.keys(): + aid_app = UICC.AID_ETSI_app_code[aid_app] + if aid_rid == (0xA0, 0x00, 0x00, 0x00, 0x87) \ + and aid_app in UICC.AID_3GPP_app_code.keys(): + aid_app = UICC.AID_3GPP_app_code[aid_app] + if aid_rid == (0xA0, 0x00, 0x00, 0x03, 0x43) \ + and aid_app in UICC.AID_3GPP2_app_code.keys(): + aid_app = UICC.AID_3GPP2_app_code[aid_app] + # get AID responsible SDO and country + if aid_rid in UICC.AID_RID.keys(): + aid_rid = UICC.AID_RID[aid_rid] + if aid_country in UICC.AID_country_code.keys(): + aid_country = UICC.AID_country_code[aid_country] + + return('%s || %s || %s || %s || %s' \ + % (aid_rid, aid_app, aid_country, aid_provider, tuple(aid[11:]))) + + def get_ICCID(self): + ''' + check EF_ICCID at the MF level, + and returnq the ASCII value of the ICCID + ''' + #go back to MF and select EF_ICCID + #self.select(addr=[]) + + # EF_ICCID is at the MF level and contains Application ID: + EF_ICCID = self.select([0x2F, 0xE2], type='pmf') + if self.dbg >= 3: + log(3, '(get_ICCID) EF_ICCID: %s' % EF_ICCID) + if EF_ICCID is None: + return None + return decode_BCD( EF_ICCID['Data'] ) + + def select_by_name(self, name=[]): + ''' + AID selection by name: should be AID bytes + ''' + return self.select(name, 'aid') + + def select_by_aid(self, aid_num=1): + ''' + AID selection by index + ''' + if hasattr(self, 'AID') and aid_num <= len(self.AID)+1: + return self.select(self.AID[aid_num-1], 'aid') + diff --git a/card/SIM.py b/card/SIM.py new file mode 100644 index 0000000..06eb458 --- /dev/null +++ b/card/SIM.py @@ -0,0 +1,456 @@ +# -*- coding: UTF-8 -*- +""" +card: Library adapted to request (U)SIM cards and other types of telco cards. +Copyright (C) 2010 Benoit Michau + +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, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" + +################################# +# Python library to work on +# SIM card +# communication based on ISO7816 card +# +# needs pyscard from: +# http://pyscard.sourceforge.net/ +################################# + +from card.ICC import ISO7816 +from card.FS import SIM_FS, MF_FS +from card.utils import * + +SIM_service_table = { + 1 : "CHV1 disable function", + 2 : "Abbreviated Dialling Numbers (ADN)", + 3 : "Fixed Dialling Numbers (FDN)", + 4 : "Short Message Storage (SMS)", + 5 : "Advice of Charge (AoC)", + 6 : "Capability Configuration Parameters (CCP)", + 7 : "PLMN selector", + 8 : "RFU", + 9 : "MSISDN", + 10 : "Extension1", + 11 : "Extension2", + 12 : "SMS Parameters", + 13 : "Last Number Dialled (LND)", + 14 : "Cell Broadcast Message Identifier", + 15 : "Group Identifier Level 1", + 16 : "Group Identifier Level 2", + 17 : "Service Provider Name", + 18 : "Service Dialling Numbers (SDN)", + 19 : "Extension3", + 20 : "RFU", + 21 : "VGCS Group Identifier List (EFVGCS and EFVGCSS)", + 22 : "VBS Group Identifier List (EFVBS and EFVBSS)", + 23 : "enhanced Multi-Level Precedence and Pre-emption Service", + 24 : "Automatic Answer for eMLPP", + 25 : "Data download via SMS-CB", + 26 : "Data download via SMS-PP", + 27 : "Menu selection", + 28 : "Call control", + 29 : "Proactive SIM", + 30 : "Cell Broadcast Message Identifier Ranges", + 31 : "Barred Dialling Numbers (BDN)", + 32 : "Extension4", + 33 : "De-personalization Control Keys", + 34 : "Co-operative Network List", + 35 : "Short Message Status Reports", + 36 : "Network's indication of alerting in the MS ", + 37 : "Mobile Originated Short Message control by SIM ", + 38 : "GPRS", + 39 : "Image (IMG)", + 40 : "SoLSA (Support of Local Service Area)", + 41 : "USSD string data object supported in Call Control", + 42 : "RUN AT COMMAND command", + 43 : "User controlled PLMN Selector with Access Technology", + 44 : "Operator controlled PLMN Selector with Access Technology", + 45 : "HPLMN Selector with Access Technology", + 46 : "CPBCCH Information", + 47 : "Investigation Scan", + 48 : "Extended Capability Configuration Parameters", + 49 : "MExE", + 50 : "RPLMN last used Access Technology", + 51 : "PLMN Network Name", + 52 : "Operator PLMN List", + 53 : "Mailbox Dialling Numbers ", + 54 : "Message Waiting Indication Status", + 55 : "Call Forwarding Indication Status", + 56 : "Service Provider Display Information", + } + +class SIM(ISO7816): + ''' + define attributes, methods and facilities for ETSI / 3GPP SIM card + check SIM specifications in ETSI TS 102.221 and 3GPP TS 51.011 + + inherit methods and objects from ISO7816 class + use self.dbg = 1 or more to print live debugging information + ''' + + def __init__(self): + ''' + initialize like an ISO7816-4 card with CLA=0xA0 + can also be used for USIM working in SIM mode, + ''' + ISO7816.__init__(self, CLA=0xA0) + + if self.dbg >= 2: + log(3, '(SIM.__init__) type definition: %s' % type(self)) + log(3, '(SIM.__init__) CLA definition: %s' % hex(self.CLA)) + + @staticmethod + def sw_status(sw1, sw2): + ''' + sw_status(sw1=int, sw2=int) -> string + + extends SW status bytes interpretation from ISO7816 + with ETSI / 3GPP SW codes + helps to speak with the smartcard! + ''' + status = ISO7816.sw_status(sw1, sw2) + if sw1 == 0x91: status = 'normal processing, with extra info ' \ + 'containing a command for the terminal: length of the ' \ + 'response data %d' % sw2 + elif sw1 == 0x9E: status = 'normal processing, SIM data download ' \ + 'error: length of the response data %d' % sw2 + elif sw1 == 0x9F: status = 'normal processing: length of the ' \ + 'response data %d' % sw2 + elif (sw1, sw2) == (0x93, 0x00): status = 'SIM application toolkit ' \ + 'busy, command cannot be executed at present' + elif sw1 == 0x92 : + status = 'memory management' + if sw2 < 16: status += ': command successful but after %d '\ + 'retry routine' % sw2 + elif sw2 == 0x40: status += ': memory problem' + elif sw1 == 0x94: + status = 'referencing management' + if sw2 == 0x00: status += ': no EF selected' + elif sw2 == 0x02: status += ': out of range (invalid address)' + elif sw2 == 0x04: status += ': file ID or pattern not found' + elif sw2 == 0x08: status += ': file inconsistent with the command' + elif sw1 == 0x98: + status = 'security management' + if sw2 == 0x02: status += ': no CHV initialized' + elif sw2 == 0x04: status += ': access condition not fulfilled, ' \ + 'at least 1 attempt left' + elif sw2 == 0x08: status += ': in contradiction with CHV status' + elif sw2 == 0x10: status += ': in contradiction with ' \ + 'invalidation status' + elif sw2 == 0x40: status += ': unsuccessful CHV verification, ' \ + 'no attempt left' + elif sw2 == 0x50: status += ': increase cannot be performed, ' \ + 'max value reached' + elif sw2 == 0x62: status += ': authentication error, ' \ + 'application specific' + elif sw2 == 0x63: status += ': security session expired' + return status + + def verify_pin(self, pin='', pin_type=1): + ''' + verify CHV1 (PIN code) or CHV2 with VERIFY APDU command + call ISO7816 VERIFY method + ''' + if pin_type in [1, 2] and type(pin) is str and \ + len(pin) == 4 and 0 <= int(pin) < 10000: + PIN = [ord(i) for i in pin] + [0xFF, 0xFF, 0xFF, 0xFF] + self.coms.push( self.VERIFY(P2=pin_type, Data=PIN) ) + else: + if self.dbg: + log(2, '(verify_pin) bad input parameters') + + def disable_pin(self, pin='', pin_type=1): + ''' + disable CHV1 (PIN code) or CHV2 with DISABLE_CHV APDU command + TIP: do it as soon as you can when you are working + with a SIM / USIM card for which you know the PIN! + call ISO7816 DISABLE method + ''' + if pin_type in [1, 2] and type(pin) is str and \ + len(pin) == 4 and 0 <= int(pin) < 10000: + PIN = [ord(i) for i in pin] + [0xFF, 0xFF, 0xFF, 0xFF] + self.coms.push( self.DISABLE_CHV(P2=pin_type, Data=PIN) ) + else: + if self.dbg: + log(2, '(disable_pin) bad input parameters') + + def enable_pin(self, pin='', pin_type=1): + ''' + enable CHV1 (PIN code) or CHV2 with ENABLE_CHV APDU command + call ISO7816 ENABLE method + ''' + if pin_type in [1, 2] and type(pin) is str and \ + len(pin) == 4 and 0 <= int(pin) < 10000: + PIN = [ord(i) for i in pin] + [0xFF, 0xFF, 0xFF, 0xFF] + self.coms.push( self.ENABLE_CHV(P2=pin_type, Data=PIN) ) + else: + if self.dbg: + log(2, '(enable_pin) bad input parameters') + + def unblock_pin(self, pin_type=1, unblock_pin=''): + ''' + WARNING: not correctly implemented!!! + and PUK are in general 8 nums... + TODO: make it correctly! + APDU Tx de-activated + + unblock CHV1 (PIN code) or CHV2 with UNBLOCK_CHV APDU command + and set 0000 value for new PIN + call ISO7816 UNBLOCK_CHV method + ''' + log(1, '(unblock_pin) not implemented: aborting') + return + #if pin_type == 1: + # pin_type = 0 + if pin_type in [0, 2] and type(unblock_pin) is str and \ + len(unblock_pin) == 4 and 0 <= int(unblock_pin) < 10000: + UNBL_PIN = [ord(i) for i in unblock_pin] + [0xFF, 0xFF, 0xFF, 0xFF] + #self.coms.push( self.UNBLOCK_CHV(P2=pin_type, Lc=0x10, \ + # Data=UNBL_PIN + \ + # [0x30, 0x30, 0x30, 0x30, 0xFF, 0xFF, 0xFF, 0xFF]) ) + else: + if self.dbg: + log(2, '(unblock_pin) bad input parameters') + #return self.UNBLOCK_CHV(P2=pin_type) + + def parse_file(self, Data=[]): + ''' + parse_file(Data=[0x12, 0x34, 0x56, 0x89]) -> dict(file) + + parses a list of bytes returned when selecting a file + interprets the content of some informative bytes for right accesses, + type / format of file... see TS 51.011 + works over the SIM file structure + ''' + fil = {} + fil['Size'] = Data[2]*0x100 + Data[3] + fil['File Identifier'] = Data[4:6] + fil['Type'] = ('RFU', 'MF', 'DF', '', 'EF')[Data[6]] + fil['Length'] = Data[12] + if fil['Type'] == 'MF' or fil['Type'] == 'DF': + fil['DF_num'] = Data[14] + fil['EF_num'] = Data[15] + fil['codes_num'] = Data[16] + fil['CHV1'] = ('not initialized','initialized')\ + [(Data[18] & 0x80) / 0x80]\ + + ': %d attempts remain' % (Data[18] & 0x0F) + fil['unblock_CHV1'] = ('not initialized','initialized')\ + [(Data[19] & 0x80) / 0x80]\ + + ': %d attempts remain' % (Data[19] & 0x0F) + fil['CHV2'] = ('not initialized','initialized')\ + [(Data[20] & 0x80) / 0x80]\ + + ': %d attempts remain' % (Data[20] & 0x0F) + fil['unblock_CHV2'] = ('not initialized','initialized')\ + [(Data[21] & 0x80) / 0x80]\ + + ': %d attempts remain' % (Data[21] & 0x0F) + if len(Data) > 23: + fil['Adm'] = Data[23:] + elif fil['Type'] == 'EF': + cond = ('ALW', 'CHV1', 'CHV2', 'RFU', 'ADM_4', 'ADM_5', + 'ADM_6', 'ADM_7', 'ADM_8', 'ADM_9', 'ADM_A', + 'ADM_B', 'ADM_C', 'ADM_D', 'ADM_E', 'NEW') + fil['UPDATE'] = cond[Data[8] & 0x0F] + fil['READ'] = cond[Data[8] >> 4] + fil['INCREASE'] = cond[Data[9] >> 4] + fil['INVALIDATE'] = cond[Data[10] & 0x0F] + fil['REHABILITATE'] = cond[Data[10] >> 4] + fil['Status'] = ('not read/updatable when invalidated', + 'read/updatable when invalidated')\ + [byteToBit(Data[11])[5]] \ + + (': invalidated',': not invalidated')\ + [byteToBit(Data[11])[7]] + fil['Structure'] = ('transparent', 'linear fixed', '', 'cyclic')\ + [Data[13]] + if fil['Structure'] == 'cyclic': + fil['INCREASE'] = byteToBit(Data[7])[1] + if len(Data) > 14: + fil['Record Length'] = Data[14] + return fil + + def run_gsm_alg(self, RAND=16*[0x00]): + ''' + self.run_gsm_alg( RAND ) -> ( SRES, Kc ) + RAND : list of bytes, length 16 + SRES : list of bytes, length 4 + Kc : list of bytes, length 8 + + runs GSM authentication algorithm: + accepts any kind of RAND (old GSM fashion) + feed with RAND 16 bytes value + returns a list with SRES and Kc, or None on error + ''' + if len(RAND) != 16: + if self.dbg: + log(1, '(run_gsm_alg) bad RAND value: aborting') + return None + # select DF_GSM directory + self.select([0x7F, 0x20]) + if self.coms()[2] != (0x90, 0x00): + if self.dbg >= 2: + log(3, '(run_gsm_alg) %s' % self.coms()) + return None + # run authentication + self.coms.push(self.INTERNAL_AUTHENTICATE(P1=0x00, P2=0x00, Data=RAND)) + if self.coms()[2][0] != 0x9F: + if self.dbg >= 2: + log(3, '(run_gsm_alg) %s' % self.coms()) + return None + # get authentication response + self.coms.push(self.GET_RESPONSE(Le=self.coms()[2][1])) + if self.coms()[2] != (0x90, 0x00): + if self.dbg >= 2: + log(3, '(run_gsm_alg) %s' % self.coms()) + return None + SRES, Kc = self.coms()[3][0:4], self.coms()[3][4:] + return [ SRES, Kc ] + + def get_imsi(self): + ''' + self.get_imsi() -> string(IMSI) + + reads IMSI value at address [0x6F, 0x07] + returns IMSI string on success or None on error + ''' + # select DF_GSM for SIM card + self.select([0x7F, 0x20]) + if self.coms()[2] != (0x90, 0x00): + if self.dbg >= 2: + log(3, '(get_imsi) %s' % self.coms()) + return None + + # select IMSI file + imsi = self.select([0x6F, 0x07]) + if self.coms()[2] != (0x90, 0x00): + if self.dbg >= 2: + log(3, '(get_imsi) %s' % self.coms()) + return None + + # and parse the received data into the IMSI structure + if 'Data' in imsi.keys() and len(imsi['Data']) == 9: + return decode_BCD(imsi['Data'])[3:] + + # if issue with the content of the DF_IMSI file + if self.dbg >= 2: + log(3, '(get_imsi) %s' % self.coms()) + return None + + def get_services(self): + ''' + self.get_services() -> None + + reads SIM Service Table at address [0x6F, 0x38] + returns list of services allowed / activated + ''' + # select DF_GSM for SIM card + self.select([0x7F, 0x20]) + if self.coms()[2] != (0x90, 0x00): + if self.dbg >= 2: + log(3, '(get_services) %s' % self.coms()) + return None + + # select SST file + sst = self.select([0x6F, 0x38]) + if self.coms()[2] != (0x90, 0x00): + if self.dbg >= 2: + log(3, '(get_services) %s' % self.coms()) + return None + + # parse data and prints corresponding services + if 'Data' in sst.keys() and len(sst['Data']) >= 2: + return self.get_services_from_sst(sst['Data']) + + def read_services(self): + ''' + self.read_services() -> None + + reads SIM Service Table at address [0x6F, 0x38] + prints services allowed / activated + returns None + ''' + serv = self.get_services() + for s in serv: + print(s) + + def get_services_from_sst(self, sst=[0, 0]): + services = [] + cnt = 0 + for B in sst: + # 2 bits per service -> 4 services per byte + for i in range(0, 7, 2): + cnt += 1 + if B & 2**i: + info = 'allocated' + if B & (2**i+1): + info += ' | activated' + if cnt in SIM_service_table: + services.append('%i : %s : %s' \ + % (cnt, SIM_service_table[cnt], info)) + else: + services.append('%i : %s' % (cnt, info)) + return services + + def explore_fs(self, filename='sim_fs', depth=True, emul=False): + ''' + self.explore_fs(self, filename='sim_fs') -> void + filename: file to write in information found + depth: depth in recursivity, True=infinite + + brute force all file addresses from MF recursively + (until no more DF are found) + write information on existing DF and file in the output file + ''' + simfs_entries = MF_FS.keys() + if not emul: + self.explore_DF([], None, depth) + + fd = open(filename, 'w') + fd.write('\n### MF ###\n') + f = self.select() + write_dict(f, fd) + fd.write('\n') + # + for f in self.FS: + path = tuple(f['Absolut Path']) + if path in simfs_entries: + f['Name'] = MF_FS[path] + write_dict(f, fd) + fd.write('\n') + + fd.close() + + def get_ICCID(self): + # select MF + self.select([0x3F, 0x0]) + if self.coms()[2] != (0x90, 0x00): + if self.dbg >= 2: + log(3, '(get_ICCID) %s' % self.coms()) + return None + + # select IMSI file + iccid = self.select([0x2F, 0xE2]) + if self.coms()[2] != (0x90, 0x00): + if self.dbg >= 2: + log(3, '(get_ICCID) %s' % self.coms()) + return None + + # and parse the received data into the IMSI structure + if 'Data' in iccid.keys() and len(iccid['Data']) >= 10: + return decode_BCD(iccid['Data']) + + # if issue with the content of the ICCID file + if self.dbg >= 2: + log(3, '(get_ICCID) %s' % self.coms()) + return None + + diff --git a/card/USIM.py b/card/USIM.py new file mode 100644 index 0000000..71b87ce --- /dev/null +++ b/card/USIM.py @@ -0,0 +1,539 @@ +# -*- coding: UTF-8 -*- +""" +card: Library adapted to request (U)SIM cards and other types of telco cards. +Copyright (C) 2010 Benoit Michau + +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, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" + + +################################# +# Python library to work on +# USIM card +# communication based on ISO7816 card +# and commands and formats based on UICC card +# +# needs pyscard from: +# http://pyscard.sourceforge.net/ +################################# + +from card.ICC import UICC, ISO7816 +from card.SIM import SIM +from card.FS import USIM_app_FS +from card.utils import * + +USIM_service_table = { + 1 : 'Local Phone Book', + 2 : 'Fixed Dialling Numbers (FDN)', + 3 : 'Extension 2', + 4 : 'Service Dialling Numbers (SDN)', + 5 : 'Extension3', + 6 : 'Barred Dialling Numbers (BDN)', + 7 : 'Extension4', + 8 : 'Outgoing Call Information (OCI and OCT)', + 9 : 'Incoming Call Information (ICI and ICT)', + 10 : 'Short Message Storage (SMS)', + 11 : 'Short Message Status Reports (SMSR)', + 12 : 'Short Message Service Parameters (SMSP)', + 13 : 'Advice of Charge (AoC)', + 14 : 'Capability Configuration Parameters 2 (CCP2)', + 15 : 'Cell Broadcast Message Identifier ', + 16 : 'Cell Broadcast Message Identifier Ranges ', + 17 : 'Group Identifier Level 1', + 18 : 'Group Identifier Level 2', + 19 : 'Service Provider Name', + 20 : 'User controlled PLMN selector with Access Technology', + 21 : 'MSISDN', + 22 : 'Image (IMG)', + 23 : 'Support of Localised Service Areas (SoLSA) ', + 24 : 'Enhanced Multi-Level Precedence and Pre-emption Service', + 25 : 'Automatic Answer for eMLPP', + 26 : 'RFU', + 27 : 'GSM Access', + 28 : 'Data download via SMS-PP', + 29 : 'Data download via SMS-CB', + 30 : 'Call Control by USIM', + 31 : 'MO-SMS Control by USIM', + 32 : 'RUN AT COMMAND command', + 33 : 'shall be set to \'1\'', + 34 : 'Enabled Services Table', + 35 : 'APN Control List (ACL)', + 36 : 'Depersonalisation Control Keys', + 37 : 'Co-operative Network List', + 38 : 'GSM security context ', + 39 : 'CPBCCH Information', + 40 : 'Investigation Scan', + 41 : 'MexE', + 42 : 'Operator controlled PLMN selector with Access Technology', + 43 : 'HPLMN selector with Access Technology', + 44 : 'Extension 5', + 45 : 'PLMN Network Name', + 46 : 'Operator PLMN List', + 47 : 'Mailbox Dialling Numbers ', + 48 : 'Message Waiting Indication Status', + 49 : 'Call Forwarding Indication Status', + 50 : 'Reserved and shall be ignored', + 51 : 'Service Provider Display Information', + 52 : 'Multimedia Messaging Service (MMS)', + 53 : 'Extension 8', + 54 : 'Call control on GPRS by USIM', + 55 : 'MMS User Connectivity Parameters', + 56 : 'Network\'s indication of alerting in the MS (NIA)', + 57 : 'VGCS Group Identifier List (EFVGCS and EFVGCSS)', + 58 : 'VBS Group Identifier List (EFVBS and EFVBSS)', + 59 : 'Pseudonym', + 60 : 'User Controlled PLMN selector for I-WLAN access', + 61 : 'Operator Controlled PLMN selector for I-WLAN access', + 62 : 'User controlled WSID list', + 63 : 'Operator controlled WSID list', + 64 : 'VGCS security', + 65 : 'VBS security', + 66 : 'WLAN Reauthentication Identity', + 67 : 'Multimedia Messages Storage', + 68 : 'Generic Bootstrapping Architecture (GBA)', + 69 : 'MBMS security', + 70 : 'Data download via USSD and USSD application mode', + 71 : 'Equivalent HPLMN', + 72 : 'Additional TERMINAL PROFILE after UICC activation', + 73 : 'Equivalent HPLMN Presentation Indication', + 74 : 'Last RPLMN Selection Indication', + 75 : 'OMA BCAST Smart Card Profile', + 76 : 'GBA-based Local Key Establishment Mechanism', + 77 : 'Terminal Applications', + 78 : 'Service Provider Name Icon', + 79 : 'PLMN Network Name Icon', + 80 : 'Connectivity Parameters for USIM IP connections', + 81 : 'Home I-WLAN Specific Identifier List', + 82 : 'I-WLAN Equivalent HPLMN Presentation Indication', + 83 : 'I-WLAN HPLMN Priority Indication', + 84 : 'I-WLAN Last Registered PLMN', + 85 : 'EPS Mobility Management Information', + 86 : 'Allowed CSG Lists and corresponding indications', + 87 : 'Call control on EPS PDN connection by USIM', + 88 : 'HPLMN Direct Access', + 89 : 'eCall Data', + 90 : 'Operator CSG Lists and corresponding indications', + 91 : 'Support for SM-over-IP', + 92 : 'Support of CSG Display Control', + 93 : 'Communication Control for IMS by USIM', + 94 : 'Extended Terminal Applications', + 95 : 'Support of UICC access to IMS', + 96 : 'Non-Access Stratum configuration by USIM', + 97 : 'PWS configuration by USIM', + } + +class USIM(UICC): + ''' + defines attributes, methods and facilities for ETSI / 3GPP USIM card + check USIM specifications in 3GPP TS 31.102 + + inherits (eventually overrides) methods and objects from UICC class + use self.dbg = 1 or more to print live debugging information + ''' + + def __init__(self): + ''' + initializes like an ISO7816-4 card with CLA=0x00 + and checks available AID (Application ID) read from EF_DIR + + initializes on the MF + ''' + # initialize like a UICC + ISO7816.__init__(self, CLA=0x00) + self.AID = [] + + if self.dbg >= 2: + log(3, '(UICC.__init__) type definition: %s' % type(self)) + log(3, '(UICC.__init__) CLA definition: %s' % hex(self.CLA)) + + self.SELECT_ADF_USIM() + + def SELECT_ADF_USIM(self): + # USIM selection from AID + if self.dbg: + log(3, '(USIM.__init__) UICC AID found:') + self.get_AID() + for aid in self.AID: + if tuple(aid[0:5]) == (0xA0, 0x00, 0x00, 0x00, 0x87) \ + and tuple(aid[5:7]) == (0x10, 0x02) : + usim = self.select(addr=aid, type='aid') + if usim is None and self.dbg: + log(2, '(USIM.__init__) USIM AID selection failed') + if usim is not None: + self.USIM_AID = aid + if self.dbg: + log(3, '(USIM.__init__) USIM AID selection succeeded\n') + + @staticmethod + def sw_status(sw1, sw2): + status = SIM.sw_status(sw1, sw2) + if sw1 == 0x98 and sw2 in (0x62, 0x64, 0x65, 0x66, 0x67): + status = 'security management' + if sw2 == 0x62: status += ': authentication error, ' \ + 'incorrect MAC' + elif sw2 == 0x64: status += ': authentication error, ' \ + 'security context not supported' + elif sw2 == 0x65: status += ': key freshness failure' + elif sw2 == 0x66: status += ': authentication error, ' \ + 'no memory space available' + elif sw2 == 0x67: status += ': authentication error, ' \ + 'no memory space available in EF_MUK' + return status + + def get_imsi(self): + ''' + get_imsi() -> string(IMSI) + + reads IMSI value at address [0x6F, 0x07] + returns IMSI string on success or None on error + ''' + # select IMSI file + imsi = self.select([0x6F, 0x07]) + if imsi is None: + return None + # and parse the received data into the IMSI structure + if 'Data' in imsi.keys() and len(imsi['Data']) == 9: + return decode_BCD(imsi['Data'])[3:] + + # if issue with the content of the DF_IMSI file + if self.dbg >= 2: + log(3, '(get_imsi) %s' % self.coms()) + return None + + def get_CS_keys(self): + ''' + get_CS_keys() -> [KSI, CK, IK] + + reads CS UMTS keys at address [0x6F, 0x08] + returns list of 3 keys, each are list of bytes, on success + (or eventually the whole file dict if the format is strange) + or None on error + ''' + EF_KEYS = self.select( [0x6F, 0x08] ) + if self.coms()[2] == (0x90, 0x00): + if len(EF_KEYS['Data']) == 33: + KSI, CK, IK = ( EF_KEYS['Data'][0:1], + EF_KEYS['Data'][1:17], + EF_KEYS['Data'][17:33]) + log(3, '(get_CS_keys) successful CS keys selection: ' \ + 'Get [KSI, CK, IK]') + return [KSI, CK, IK] + else: + return EF_KEYS + return None + + def get_PS_keys(self): + ''' + get_PS_keys() -> [KSI, CK_PS, IK_PS] + + reads PS UMTS keys at address [0x6F, 0x09] + returns list of 3 keys, each are list of bytes, on success + (or eventually the whole file dict if the format is strange) + or None on error + ''' + EF_KEYSPS = self.select( [0x6F, 0x09] ) + if self.coms()[2] == (0x90, 0x00): + if len(EF_KEYSPS['Data']) == 33: + KSI, CK, IK = ( EF_KEYSPS['Data'][0:1], + EF_KEYSPS['Data'][1:17], + EF_KEYSPS['Data'][17:33] ) + log(3, '(get_PS_keys) successful PS keys selection: ' \ + 'Get [KSI, CK, IK]') + return [KSI, CK, IK] + else: + return EF_KEYSPS + return None + + def get_GBA_BP(self): + ''' + get_GBA_BP() -> [[RAND, B-TID, KeyLifetime], ...], + Length-Value parsing style + + reads EF_GBABP file at address [0x6F, 0xD6], + containing RAND and associated B-TID and KeyLifetime + returns list of list of bytes on success + (or eventually the whole file dict if the format is strange) + or None on error + ''' + EF_GBABP = self.select( [0x6F, 0xD6] ) + if self.coms()[2] == (0x90, 0x00): + if len(EF_GBABP['Data']) > 2: + #RAND, B_TID, Lifetime = LV_parser( EF_GBABP['Data'] ) + log(3, '(get_GBA_BP) successful GBA_BP selection: ' \ + 'Get list of [RAND, B-TID, KeyLifetime]') + #return (RAND, B_TID, Lifetime) + return LV_parser( EF_GBABP['Data'] ) + else: + return EF_GBABP + return None + + def update_GBA_BP(self, RAND, B_TID, key_lifetime): + ''' + update_GBA_BP([RAND], [B_TID], [key_lifetime]) + -> void (or EF_GBABP file dict if RAND not found) + + reads EF_GBABP file at address [0x6F, 0xD6], + checks if RAND provided is referenced, + and updates the file structure with provided B-TID and KeyLifetime + returns nothing (or eventually the whole file dict + if the RAND is not found) + ''' + GBA_BP = self.get_GBA_BP() + for i in GBA_BP: + if i == RAND: + log(3, '(update_GBA_BP) RAND found in GBA_BP') + # update transparent file with B_TID and key lifetime + self.coms.push( self.UPDATE_BINARY( P2=len(RAND)+1, + Data=[len(B_TID)] + B_TID + \ + [len(key_lifetime)] + key_lifetime )) + if self.dbg >= 2: + log(3, '(update_GBA_BP) %s' % self.coms()) + if self.coms()[2] == 0x90 and self.dbg: + log(3, '(update_GBA_BP) successful GBA_BP update with ' \ + 'B-TID and key lifetime') + if self.dbg >= 3: + log(3, '(update_GBA_BP) new value of EF_GBA_BP:\n%s' \ + % self.get_GBA_BP()) + else: + if self.dbg: + log(2, '(update_GBA_BP) RAND not found in GBA_BP') + return GBA_BP + + def get_GBA_NL(self): + ''' + get_GBA_NL() -> [[NAF_ID, B-TID], ...] , TLV parsing style + + reads EF_GBANL file at address [0x6F, 0xDA], containing NAF_ID and B-TID + returns list of list of bytes vector on success + (or eventually the whole file dict if the format is strange) + or None on error + ''' + EF_GBANL = self.select( [0x6F, 0xDA] ) + if self.coms()[2] == (0x90, 0x00): + if len(EF_GBANL['Data'][0]) > 2: + # This is Tag-Length-Value parsing, + # with 0x80 for NAF_ID and 0x81 for B-TID + values = [] + + for rec in EF_GBANL['Data']: + NAF_ID, B_TID = [], [] + while len(rec) > 0: + tlv = first_TLV_parser( rec ) + if tlv[1] > 0xFF: + rec = rec[ tlv[1]+4 : ] + else: + rec = rec[ tlv[1]+2 : ] + if tlv[0] == 0x80: + NAF_ID = tlv[2] + elif tlv[0] == 0x81: + B_TID = tlv[2] + values.append( [NAF_ID, B_TID] ) + + log(3, '(get_GBA_NL) Successful GBA_NL selection: ' \ + 'Get list of [NAF_ID, B-TID]') + #return (NAF_ID, B_TID) + return values + else: + return EF_GBANL + return None + + def authenticate(self, RAND=[], AUTN=[], ctx='3G'): + ''' + self.authenticate(RAND, AUTN, ctx='3G') -> [key1, key2...], + LV parsing style + + runs the INTERNAL AUTHENTICATE command in the USIM + with the right context: + ctx = '2G', '3G', 'GBA' ('MBMS' or other not supported at this time) + RAND and AUTN are list of bytes; for '2G' context, AUTN is not used + returns a list containing the keys (list of bytes) computed in the USIM, + on success: + [RES, CK, IK (, Kc)] or [AUTS] for '3G' + [RES] or [AUTS] for 'GBA' + [RES, Kc] for '2G' + or None on error + ''' + # prepare input data for authentication + if ctx in ('3G', 'VGCS', 'GBA', 'MBMS') and len(RAND) != 16 \ + and len(AUTN) != 16: + log(1, '(authenticate) bad AUTN parameter: aborting') + return None + + inp = [] + if ctx == '3G': + P2 = 0x81 + elif ctx == 'VGCS': + P2 = 0x82 + log(1, '(authenticate) VGCS auth not implemented: aborting') + return None + elif ctx == 'MBMS': + log(1, '(authenticate) MBMS auth not implemented: aborting') + return None + elif ctx == 'GBA': + P2 = 0x84 + inp = [0xDD] + inp.extend( [len(RAND)] + RAND + [len(AUTN)] + AUTN ) + if ctx not in ['3G', 'VGCS', 'MBMS', 'GBA']: + # and also, if ctx == '2G'... the safe way + # to avoid desynchronizing our USIM counter + P2 = 0x80 + if len(RAND) != 16: + log(1, '(authenticate) bad RAND parameter: aborting') + return None + # override input value for 2G authent + inp = [len(RAND)] + RAND + + self.coms.push( self.INTERNAL_AUTHENTICATE(P2=P2, Data=inp) ) + if self.coms()[2][0] in (0x9F, 0x61): + self.coms.push( self.GET_RESPONSE(Le=self.coms()[2][1]) ) + if self.coms()[2] == (0x90, 0x00): + val = self.coms()[3] + if P2 == 0x80: + if self.dbg: + log(3, '(authenticate) successful 2G authentication. ' \ + 'Get [RES, Kc]') + values = LV_parser(val) + # returned values are (RES, Kc) + return values + # not adapted to 2G context with Kc, RES: to be confirmed... + if val[0] == 0xDB: + if P2 == 0x81 and self.dbg: + log(3, '(authenticate) successful 3G authentication. ' \ + 'Get [RES, CK, IK(, Kc)]') + elif P2 == 0x84 and self.dbg: + log(3, '(authenticate) successful GBA authentication.' \ + ' Get [RES]') + values = LV_parser(val[1:]) + # returned values can be (RES, CK, IK) or (RES, CK, IK, Kc) + return values + elif val[0] == 0xDC: + if self.dbg: + log(2, '(authenticate) synchronization failure. ' \ + 'Get [AUTS]') + values = LV_parser(val[1:]) + return values + #else: + if self.dbg: + log(1, '(authenticate) error: %s' % self.coms()) + return None + + def GBA_derivation(self, NAF_ID=[], IMPI=[]): + ''' + self.GBA_derivation(NAF_ID, IMPI) -> [Ks_ext_naf] + + runs the INTERNAL AUTHENTICATE command in the USIM + with the GBA derivation context: + NAF_ID is a list of bytes (use stringToByte()) + "NAF domain name"||"security protocol id", + eg: "application.org"||"0x010001000a" (> TLS with RSA and SHA) + IMPI is a list of bytes + "IMSI@ims.mncXXX.mccYYY.3gppnetwork.org" if no IMS IMPI + is specifically defined in the USIM + returns a list with GBA ext key (list of bytes) computed in the USIM: + [Ks_ext_naf] + Ks_int_naf remains available in the USIM + for further GBA_U key derivation + or None on error + + see TS 33.220 for GBA specific formats + ''' + # need to run 1st an authenicate command with 'GBA' context, + # so to have the required keys in the USIM + P2 = 0x84 + inp = [0xDE] + [len(NAF_ID)] + NAF_ID + [len(IMPI)] + IMPI + + self.coms.push( self.INTERNAL_AUTHENTICATE(P2=P2, Data=inp) ) + if self.coms()[2][0] in (0x9F, 0x61): + self.coms.push( self.GET_RESPONSE(Le=self.coms()[2][1]) ) + if self.coms()[2] == (0x90, 0x00): + val = self.coms()[3] + if val[0] == 0xDB: # not adapted to 2G context with Kc, RES + if self.dbg: + log(3, '(GBA_derivation) successful GBA derivation. ' \ + 'Get [Ks_EXT_NAF]') + values = LV_parser(val[1:]) + return values + if self.dbg: + log(3, '(GBA_derivation) authentication failure: %s' % self.coms()) + return None + + def get_services(self): + ''' + self.get_services() -> None + + reads USIM Service Table at address [0x6F, 0x38] + prints services allowed / activated + returns None + ''' + # select SST file + sst = self.select([0x6F, 0x38]) + if self.coms()[2] != (0x90, 0x00): + if self.dbg >= 2: + log(3, '(get_services) %s' % self.coms()) + return None + + # parse data and prints corresponding services + if 'Data' in sst.keys() and len(sst['Data']) >= 2: + return self.get_services_from_sst(sst['Data']) + + def read_services(self): + serv = self.get_services() + for s in serv: + print(s) + + def get_services_from_sst(self, sst=[0, 0]): + services = [] + cnt = 0 + for B in sst: + # 1 bit per service -> 8 services per byte + for i in range(0, 8): + cnt += 1 + if B & 2**i: + if cnt in USIM_service_table: + services.append('%i : %s : available' \ + % (cnt, USIM_service_table[cnt])) + else: + services.append('%i : available' % cnt) + return services + + def explore_fs(self, filename='usim_fs', depth=2): + ''' + self.explore_fs(self, filename='usim_fs') -> void + filename: file to write in information found + depth: depth in recursivity, True=infinite + + brute force all file addresses from 1st USIM AID + with a maximum recursion level (to avoid infinite looping...) + write information on existing DF and file in the output file + ''' + usimfs_entries = USIM_app_FS.keys() + self.explore_DF([], self.AID.index(self.USIM_AID)+1, depth) + + fd = open(filename, 'w') + fd.write('\n### AID %s ###\n' % self.USIM_AID) + f = self.select_by_aid( self.AID.index(self.USIM_AID)+1 ) + write_dict(f, fd) + fd.write('\n') + # + for f in self.FS: + path = tuple(f['Absolut Path']) + if path in usimfs_entries: + f['Name'] = USIM_app_FS[path] + write_dict(f, fd) + fd.write('\n') + + fd.close() +# + diff --git a/card/__init__.py b/card/__init__.py new file mode 100644 index 0000000..da9bc17 --- /dev/null +++ b/card/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: UTF-8 -*- +""" +card: Library adapted to request (U)SIM cards and other types of telco cards. +Copyright (C) 2010 Benoit Michau + +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, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" + +# smartcard Integrated Circuit Card library +# based on Laurent Rousseau pcsclite daemon or Microsoft scard service +# and Jean-Daniel Aussel pyscard (magical) python binding +# specificities of SIM and USIM card available + +__all__ = ['utils', 'ICC', 'SIM', 'USIM', 'FS'] +__version__ = '0.2.0' diff --git a/card/utils.py b/card/utils.py new file mode 100644 index 0000000..598cd4f --- /dev/null +++ b/card/utils.py @@ -0,0 +1,334 @@ +# -*- coding: UTF-8 -*- +""" +card: Library adapted to request (U)SIM cards and other types of telco cards. +Copyright (C) 2010 Benoit Michau + +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, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" + +################################# +# generic functions # +# being used in smartcard specs # +################################# + +from collections import deque +from smartcard.util import toBytes + + +############### +# log wrapper # +############### +log_levels = {1:'ERR', 2:'WNG', 3:'DBG'} +def log(level, string): + # could output to a file + # but here, just print() + print('[%s] %s' % (log_levels[level], string)) + +# from python 2.6, format('b') allows to use 0b10010110 notation: +# much convinient +def byteToBit(byte): + ''' + byteToBit(0xAB) -> [1, 0, 1, 0, 1, 0, 1, 1] + + converts a byte integer value into a list of bits + ''' + bit = [0, 0, 0, 0, 0, 0, 0, 0] + for i in range(8): + if byte % pow(2, i+1): + bit[7-i] = 1 + byte = byte - pow(2, i) + return bit + +# equivalent to the pyscard function "toASCIIBytes" +# new version of python (>2.6) seems to have a built-in "bytes" type +def stringToByte(string): + ''' + stringToByte('test') -> [116, 101, 115, 116] + + converts a string into a list of bytes + ''' + bytelist = [] + for c in string: + bytelist.extend( toBytes(c.encode('hex')) ) + return bytelist + +# equivalent to the pyscard function "toASCIIString" +def byteToString(bytelist): + ''' + byteToString([116, 101, 115, 116]) -> 'test' + + converts a list of bytes into a string + ''' + string = '' + for b in bytelist: + string += chr(b) + return string + +def LV_parser(bytelist): + ''' + LV_parser([0x02, 0xAB, 0xCD, 0x01, 0x12, 0x34]) -> [[171, 205], [18], []] + + parses Length-Value records in a list of bytes + returns a list of list of bytes + length coded on 1 byte + ''' + values = [] + while len(bytelist) > 0: + l = bytelist[0] + values.append( bytelist[1:1+l] ) + bytelist = bytelist[1+l:] + return values + +def first_TLV_parser(bytelist): + ''' + first_TLV_parser([0xAA, 0x02, 0xAB, 0xCD, 0xFF, 0x00]) -> (170, 2, [171, 205]) + + parses first TLV format record in a list of bytelist + returns a 3-Tuple: Tag, Length, Value + Value is a list of bytes + parsing of length is ETSI'style 101.220 + ''' + Tag = bytelist[0] + if bytelist[1] == 0xFF: + Len = bytelist[2]*256 + bytelist[3] + Val = bytelist[4:4+Len] + else: + Len = bytelist[1] + Val = bytelist[2:2+Len] + return (Tag, Len, Val) + +def TLV_parser(bytelist): + ''' + TLV_parser([0xAA, ..., 0xFF]) -> [(T, L, [V]), (T, L, [V]), ...] + + loops on the input list of bytes with the "first_TLV_parser()" function + returns a list of 3-Tuples + ''' + ret = [] + while len(bytelist) > 0: + T, L, V = first_TLV_parser(bytelist) + if T == 0xFF: + # padding bytes + break + ret.append( (T, L, V) ) + # need to manage length of L + if L > 0xFE: + bytelist = bytelist[ L+4 : ] + else: + bytelist = bytelist[ L+2 : ] + return ret + +def first_BERTLV_parser(bytelist): + ''' + first_BERTLV_parser([0xAA, 0x02, 0xAB, 0xCD, 0xFF, 0x00]) + -> ([1, 'contextual', 'constructed', 10], [1, 2], [171, 205]) + + parses first BER-TLV format record in a list of bytes + returns a 3-Tuple: Tag, Length, Value + Tag: [Tag class, Tag DO, Tag number] + Length: [Length of length, Length value] + Value: [Value bytes list] + parsing of length is ETSI'style 101.220 + ''' + # Tag class and DO + byte0 = byteToBit(bytelist[0]) + if byte0[0:2] == [0, 0]: + Tag_class = 'universal' + elif byte0[0:2] == [0, 1]: + Tag_class = 'applicative' + elif byte0[0:2] == [1, 0]: + Tag_class = 'contextual' + elif byte0[0:2] == [1, 1]: + Tag_class = 'private' + if byte0[2:3] == [0]: + Tag_DO = 'primitive' + elif byte0[2:3] == [1]: + Tag_DO = 'constructed' + # Tag coded with more than 1 byte + i = 0 + if byte0[3:8] == [1, 1, 1, 1, 1]: + Tag_bits = byteToBit(bytelist[1])[1:8] + i += 1 + while byteToBit(bytelist[i])[0] == 1: + i += 1 + Tag_bits += byteToBit(bytelist[i])[1:8] + # Tag coded with 1 byte + else: + Tag_bits = byte0[3:8] + + # Tag number calculation + Tag_num = 0 + for j in range(len(Tag_bits)): + Tag_num += Tag_bits[len(Tag_bits)-j-1] * pow(2, j) + + # Length coded with more than 1 byte (BER long form) + if bytelist[i+1] & 0x80: + Len_num = bytelist[i+1] - 0x80 + Len = reduce(lambda x,y: (x<<8)+y, bytelist[i+2:i+2+Len_num]) + Val = bytelist[i+2+Len_num:i+2+Len_num+Len] + # Length coded with 1 byte + else: + Len_num = 1 + Len = bytelist[i+1] + Val = bytelist[i+2:i+2+Len] + + return ([i+1, Tag_class, Tag_DO, Tag_num], [Len_num, Len], Val) + #return ([Tag_class, Tag_DO, Tag_num], Len, Val) + +def BERTLV_parser(bytelist): + ''' + BERTLV_parser([0xAA, ..., 0xFF]) -> [([T], L, [V]), ([T], L, [V]), ...] + + loops on the input bytes with the "first_BERTLV_parser()" function + returns a list of 3-Tuples containing BERTLV records + ''' + ret = [] + while len(bytelist) > 0: + T, L, V = first_BERTLV_parser(bytelist) + #if T == 0xFF: + # break # padding bytes + ret.append( (T[1:], L[1], V) ) + # need to manage lengths of Tag and Length + bytelist = bytelist[ T[0] + L[0] + L[1] : ] + return ret + +def decode_BCD(data=[]): + ''' + decode_BCD([0x21, 0xFE, 0xA3]) -> '121415310' + + to decode serial number (IMSI, ICCID...) from list of bytes + ''' + string = '' + for B in data: + # 1st digit (4 LSB), can be padding (e.g. 0xF) + if (B&0x0F) < 10: string += str(B&0x0F) + # 2nd digit (4 MSB), can be padding (e.g. 0xF) + if (B>>4) < 10: string += str(B>>4) + return string + +def compute_luhn(digit_str=''): + ''' + compute_luhn('15632458') -> 4 + + return the luhn code of the digits provided + ''' + if not digit_str.isdigit(): + print('you must provide a string of digits') + return + # append 0 + d = [int(c) for c in digit_str+'0'] + # sum of odd digits + cs = sum(d[-1::-2]) + # sum of (sum of digits(even digits * 2)) + cs += sum([(v*2)%9 if v != 9 else 9 for v in d[-2::-2]]) + # modulo 10: luhn checksum + cs = cs%10 + # return the luhn code + if cs == 0: return cs + else: return 10-cs + +def write_dict(dict, fd): + ''' + write a dict() content to a file descriptor + ''' + keys = dict.keys() + keys.sort() + fd.write('\n') + for k in keys: + rec = dict[k] + if isinstance(rec, list) and \ + len(rec) == [isinstance(i, int) for i in rec].count(True): + rec = ''.join(['[', ', '.join(map(hex, rec)), ']']) + fd.write('%s: %s\n' % (k, rec)) + +def make_graph(FS, master_name='(0x3F, 0x00)\nMF'): + try: + import pydot + except: + log(1, '(make_graph) pydot library not found: aborting') + return + #return + # build a graph using graphviz Dot language, with pydot + # create a graph with master MF or AID node + nodes={} + graph = pydot.Dot(graph_type='digraph', rankdir='LR') + nodes['master'] = pydot.Node(master_name, style='filled', fillcolor='green') + graph.add_node(nodes['master']) + # attach nodes under master + for file in FS: + abspath = file['Absolut Path'] + # build file name for the node + label = ''.join(( \ + ''.join((file['Name'], '\n')) if 'Name' in file.keys() else '', \ + '(', hex(abspath[-2]), ' ', hex(abspath[-1]), ')')) + # check for EF or DF for node color + color='yellow' if file['Type'][:2] == 'EF' else 'blue' + # make node + nodes['%s'%abspath] = pydot.Node('%s'%abspath, label=label,\ + style='filled', fillcolor=color) + # put it in the graph and append it to the parent + graph.add_node(nodes['%s'%abspath]) + graph.add_edge(pydot.Edge(nodes['%s'%abspath[:-2]] if \ + len(abspath) >= 4 else nodes['master'], \ + nodes['%s'%abspath])) + # and return graph ... + return graph + + + +####################################################### +# Generic class to keep track of sent / received APDU # +####################################################### +class apdu_stack: + ''' + input / output wrapping class + for APDU communications + + allows to keep track of communications + and exchanged commands + + based on the python "deque" fifo-like object + ''' + + def __init__(self, limit=10): + ''' + initializes apdu_stack with the maximum of IO to keep track of + ''' + self.apdu_stack = deque([], limit) + + def push(self, apdu_response): + ''' + stacks the returned response into the apdu_stack + ''' + self.apdu_stack.append( apdu_response ) + + def __repr__(self): + ''' + represents the whole stack of responses pushed on + ''' + s = '' + for apdu in self.apdu_stack: + s += apdu.__repr__() + '\n' + return s + + def __call__(self): + ''' + calling the apdu_stack returns the last response pushed on it + ''' + try: + return self.apdu_stack[-1] + except IndexError: + return None +