Back to project page smartcard-reader.
The source code is released under:
GNU General Public License
If you think the Android project smartcard-reader listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.
/* * Copyright 2014 Ryan Jones// w w w. j a v a 2 s . c o m * Copyright 2010 sasc * * This file was modified from the original source: * https://code.google.com/p/javaemvreader/ * * This file is part of smartcard-reader, package org.docrj.smartcard.reader. * * smartcard-reader 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 3 of the License, or (at your * option) any later version. * * smartcard-reader 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 smartcard-reader. If not, see <http://www.gnu.org/licenses/>. */ package org.docrj.smartcard.emv; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.IOException; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Properties; //import javax.security.auth.callback.Callback; //import javax.security.auth.callback.CallbackHandler; //import javax.security.auth.callback.PasswordCallback; //import javax.security.auth.callback.UnsupportedCallbackException; import org.docrj.smartcard.iso7816.Tag; import org.docrj.smartcard.iso7816.TagAndLength; import org.docrj.smartcard.iso7816.TagImpl; import org.docrj.smartcard.iso7816.TagValueType; import org.docrj.smartcard.util.ISO4217_Numeric; import org.docrj.smartcard.util.Util; import android.util.Log; /** * Point of sale (POS) terminal */ public class EMVTerminal { private final static String TAG = "smartcard-reader"; private final static Properties defaultTerminalProperties = new Properties(); private final static Properties runtimeTerminalProperties = new Properties(); private final static TerminalVerifResults terminalVerifResults = new TerminalVerifResults(); // private static CallbackHandler pinCallbackHandler; // private static boolean doVerifyPinIfRequired = false; // private static boolean isOnline = true; static { try { //Default properties defaultTerminalProperties.load(EMVTerminal.class.getResourceAsStream("/terminal.properties")); for (String key : defaultTerminalProperties.stringPropertyNames()) { //Sanitize String sanitizedKey = Util.byteArrayToHexString(Util.fromHexString(key)).toLowerCase(); String sanitizedValue = Util.byteArrayToHexString(Util.fromHexString(defaultTerminalProperties.getProperty(key))).toLowerCase(); defaultTerminalProperties.setProperty(sanitizedKey, sanitizedValue); } //Runtime/overridden properties String runtimeTerminalPropertiesFile = System.getProperty("terminal.properties"); if (runtimeTerminalPropertiesFile != null) { runtimeTerminalProperties.load(new FileInputStream(runtimeTerminalPropertiesFile)); for(String key : runtimeTerminalProperties.stringPropertyNames()) { //Sanitize String sanitizedKey = Util.byteArrayToHexString(Util.fromHexString(key)).toLowerCase(); String sanitizedValue = Util.byteArrayToHexString(Util.fromHexString(runtimeTerminalProperties.getProperty(key))).toLowerCase(); if(defaultTerminalProperties.contains(sanitizedKey) && sanitizedValue.length() != defaultTerminalProperties.getProperty(key).length()) { //Attempt to set different length for a default value throw new RuntimeException("Attempted to set a value with unsupported length for key: "+sanitizedKey + " (value: "+sanitizedValue+")"); } runtimeTerminalProperties.setProperty(sanitizedKey, sanitizedValue); } } } catch (IOException ioe) { throw new RuntimeException(ioe); } } //PDOL (Processing options Data Object List) //DDOL (*Default* Dynamic Data Authentication Data Object List) // (Default to be used for constructing the INTERNAL AUTHENTICATE command if the DDOL in the card is not present) //TDOL (*Default* Transaction Certificate Data Object List) // (Default to be used for generating the TC Hash Value if the TDOL in the card is not present) //PDOL example (Visa Electron, contactless) // 9f 38 18 -- Processing Options Data Object List (PDOL) // 9f 66 04 -- Terminal Transaction Qualifiers // 9f 02 06 -- Amount, Authorised (Numeric) // 9f 03 06 -- Amount, Other (Numeric) // 9f 1a 02 -- Terminal Country Code // 95 05 -- Terminal Verification Results (TVR) // 5f 2a 02 -- Transaction Currency Code // 9a 03 -- Transaction Date // 9c 01 -- Transaction Type // 9f 37 04 -- Unpredictable Number private static byte[] getTerminalResidentData(TagAndLength tal, EMVApp app) { //Check if the value is specified in the runtime properties file String propertyValueStr = runtimeTerminalProperties.getProperty(Util.byteArrayToHexString(tal.getTag().getTagBytes()).toLowerCase()); if(propertyValueStr != null) { byte[] propertyValue = Util.fromHexString(propertyValueStr); if (propertyValue.length == tal.getLength()) { return propertyValue; } } if (tal.getTag().equals(EMVTags.TERMINAL_COUNTRY_CODE) && tal.getLength() == 2) { return findCountryCode(app); } else if (tal.getTag().equals(EMVTags.TRANSACTION_CURRENCY_CODE) && tal.getLength() == 2) { return findCurrencyCode(app); } //Now check for default values propertyValueStr = defaultTerminalProperties.getProperty(Util.byteArrayToHexString(tal.getTag().getTagBytes()).toLowerCase()); if(propertyValueStr != null) { byte[] propertyValue = Util.fromHexString(propertyValueStr); if (propertyValue.length == tal.getLength()) { return propertyValue; } } if (tal.getTag().equals(EMVTags.UNPREDICTABLE_NUMBER)) { return Util.generateRandomBytes(tal.getLength()); } else if (tal.getTag().equals(EMVTags.TERMINAL_TRANSACTION_QUALIFIERS) && tal.getLength() == 4) { //This seems only to be used in contactless mode. Construct accordingly TerminalTranQualifiers ttq = new TerminalTranQualifiers(); ttq.setContactlessEMVmodeSupported(true); ttq.setReaderIsOfflineOnly(true); return ttq.getBytes(); } else if (tal.getTag().equals(EMVTags.TERMINAL_VERIFICATION_RESULTS) && tal.getLength() == 5) { //All bits set to '0' return terminalVerifResults.toByteArray(); } else if (tal.getTag().equals(EMVTags.TRANSACTION_DATE) && tal.getLength() == 3) { return Util.getCurrentDateAsNumericEncodedByteArray(); } else if (tal.getTag().equals(EMVTags.TRANSACTION_TYPE) && tal.getLength() == 1) { //transactionTypes = { 0: "Payment", 1: "Withdrawal", } //http://www.codeproject.com/Articles/100084/Introduction-to-ISO-8583 return new byte[]{0x00}; } else { Log.d(TAG, "Terminal Resident Data not found for " + tal); } byte[] defaultResponse = new byte[tal.getLength()]; Arrays.fill(defaultResponse, (byte) 0x00); return defaultResponse; } public static TerminalVerifResults getTerminalVerifResults() { return terminalVerifResults; } public static void resetTVR(){ terminalVerifResults.reset(); } public static void setProperty(String tagHex, String valueHex) { setProperty(new TagImpl(tagHex, TagValueType.BINARY, "", ""), Util.fromHexString(valueHex)); } public static void setProperty(Tag tag, byte[] value){ runtimeTerminalProperties.setProperty(Util.byteArrayToHexString(tag.getTagBytes()).toLowerCase(Locale.US), Util.byteArrayToHexString(value)); } public static boolean isCDASupported(EMVApp app) { return false; } public static boolean isDDASupported(EMVApp app) { return true; } public static boolean isSDASupported(EMVApp app) { return true; } public static boolean isATM() { return false; } public static Date getCurrentDate() { return new Date(); } /* public static int getSupportedApplicationVersionNumber(EMVApp app) { //TODO //For now, just return the version number maintained in the card //return app.getApplicationVersionNumber(); return 1; } public static boolean isCVMRecognized(EMVApp app, CVRule rule) { switch(rule.getRule()) { case RESERVED_FOR_USE_BY_THE_INDIVIDUAL_PAYMENT_SYSTEMS: //app.getAID().getRIDBytes(); //TODO check if RID specific rule is supported //if(supported) { // return true; //} case RESERVED_FOR_USE_BY_THE_ISSUER: if(app.getIssuerIdentificationNumber() != null){ //TODO check if issuer specific rule is supported //if(supported){ // return true; //} } case NOT_AVAILABLE_FOR_USE: case RFU: return false; } return true; } public static boolean isCVMSupported(CVRule rule) { switch(rule.getRule()) { //TODO support enciphered PIN case ENCIPHERED_PIN_VERIFIED_BY_ICC: case ENCIPHERED_PIN_VERIFIED_BY_ICC_AND_SIGNATURE_ON_PAPER: return false; case PLAINTEXT_PIN_VERIFIED_BY_ICC_AND_SIGNATURE_ON_PAPER: case PLAINTEXT_PIN_VERIFIED_BY_ICC: return hasPinInputCapability(); case SIGNATURE_ON_PAPER: return false; case ENCIPHERED_PIN_VERIFIED_ONLINE: return isOnline(); case FAIL_PROCESSING: case NO_CVM_REQUIRED: return true; } return false; } public static boolean isOnline() { return isOnline; } public static void setIsOnline(boolean value){ isOnline = value; } public static boolean isCVMConditionSatisfied(CVRule rule) { if(rule.getConditionAlways()) { return true; } if(rule.getConditionCode() <= 0x05){ //TODO return true; }else if(rule.getConditionCode() < 0x0A) { //TODO //Check for presence Application Currency Code or Amount, Authorised in app records? return true; } else { //RFU and proprietary return false; } } public static boolean verifyEncipheredPinOnline() { if(!isOnline()) { return false; } //TODO return true; } public static boolean hasSignatureOnPaper() { return true; } public static void setDoVerifyPinIfRequired(boolean value) { doVerifyPinIfRequired = value; } public static boolean getDoVerifyPinIfRequired() { return doVerifyPinIfRequired; } */ /** * * @return true if a Pin CallbackHandler has be set */ /* public static boolean hasPinInputCapability() { return doVerifyPinIfRequired && pinCallbackHandler != null; } public static void setPinCallbackHandler(CallbackHandler callbackHandler) { pinCallbackHandler = callbackHandler; } public static PasswordCallback getPinInput() { CallbackHandler callBackHandler = pinCallbackHandler; if(callBackHandler == null){ return null; } PasswordCallback passwordCallback = new PasswordCallback("Type PIN", false); try{ callBackHandler.handle(new Callback[]{passwordCallback}); }catch(IOException ex){ Log.info(Util.getStackTrace(ex)); }catch(UnsupportedCallbackException ex){ Log.info(Util.getStackTrace(ex)); } return passwordCallback; } public static boolean getPerformTerminalRiskManagement() { return false; } */ public static byte[] constructDOLResponse(DOL dol, EMVApp app) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); for (TagAndLength tagAndLength : dol.getTagAndLengthList()) { byte[] data = getTerminalResidentData(tagAndLength, app); stream.write(data, 0, data.length); } return stream.toByteArray(); } //The ICC may contain the DDOL, but there shall be a default DDOL in the terminal, //specified by the payment system, for use in case the DDOL is not present in the ICC. public static byte[] getDefaultDDOLResponse(EMVApp app) { //It is mandatory that the DDOL contains the Unpredictable Number generated by the terminal (tag '9F37', 4 bytes binary). byte[] unpredictableNumber = Util.generateRandomBytes(4); //TODO add other DDOL data specified by the payment system //if(app.getAID().equals(SOMEAID)) return unpredictableNumber; } //Ex Banco BRADESCO (f0 00 00 00 03 00 01) failes GPO with wrong COUNTRY_CODE ! private static byte[] findCountryCode(EMVApp app) { if(app != null){ if(app.getIssuerCC() != -1){ byte[] countryCode = Util.intToBinaryEncodedDecimalByteArray(app.getIssuerCC()); return Util.resizeArray(countryCode, 2); } } Log.d(TAG, "No Issuer Country Code found in app. Using default Terminal Country Code"); String countryCode = defaultTerminalProperties.getProperty(Util.byteArrayToHexString(EMVTags.TERMINAL_COUNTRY_CODE.getTagBytes())); if(countryCode != null){ return Util.fromHexString(countryCode); } return new byte[]{0x08, 0x26}; } private static byte[] findCurrencyCode(EMVApp app){ if(app != null){ int appCurrencyCode = app.getAppCurrencyCode(); if (appCurrencyCode != -1) { byte[] currencyCode = Util.intToBinaryEncodedDecimalByteArray(appCurrencyCode); return Util.resizeArray(currencyCode, 2); } Locale preferredLocale = null; if (app.getLanguagePref() != null) { preferredLocale = app.getLanguagePref().getPreferredLocale(); } /* TODO: if (preferredLocale == null && app.getCard() != null && app.getCard().getPSE() != null && app.getCard().getPSE().getLanguagePreference() != null){ preferredLocale = app.getCard().getPSE().getLanguagePreference().getPreferredLocale(); } */ if (preferredLocale != null) { if(preferredLocale.getLanguage().equals(Locale.getDefault().getLanguage())) { //Guesstimate; we presume default locale is the preferred preferredLocale = Locale.getDefault(); } List<Integer> numericCodes = ISO4217_Numeric.getNumericCodeForLocale(preferredLocale); if (numericCodes != null && numericCodes.size() > 0) { //Just use the first found. It might not be correct, eg Brazil (BRZ) vs Portugal (EUR) return Util.resizeArray(Util.intToBinaryEncodedDecimalByteArray(numericCodes.get(0)), 2); } } } String currencyCode = defaultTerminalProperties.getProperty(Util.byteArrayToHexString(EMVTags.TRANSACTION_CURRENCY_CODE.getTagBytes())); if(currencyCode != null){ return Util.fromHexString(currencyCode); } return new byte[]{0x08, 0x26}; } }