
com.github.devnied.emvnfccard.parser.EmvParser Maven / Gradle / Ivy
package com.github.devnied.emvnfccard.parser;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.devnied.emvnfccard.enums.CommandEnum;
import com.github.devnied.emvnfccard.enums.EmvCardScheme;
import com.github.devnied.emvnfccard.enums.SwEnum;
import com.github.devnied.emvnfccard.exception.CommunicationException;
import com.github.devnied.emvnfccard.iso7816emv.EmvTags;
import com.github.devnied.emvnfccard.iso7816emv.EmvTerminal;
import com.github.devnied.emvnfccard.iso7816emv.TagAndLength;
import com.github.devnied.emvnfccard.model.Afl;
import com.github.devnied.emvnfccard.model.EmvCard;
import com.github.devnied.emvnfccard.model.EmvTransactionRecord;
import com.github.devnied.emvnfccard.model.enums.CurrencyEnum;
import com.github.devnied.emvnfccard.utils.CommandApdu;
import com.github.devnied.emvnfccard.utils.ResponseUtils;
import com.github.devnied.emvnfccard.utils.TlvUtil;
import fr.devnied.bitlib.BitUtils;
import fr.devnied.bitlib.BytesUtils;
/**
* Emv Parser.
* Class used to read and parse EMV card
*
* @author MILLAU Julien
*
*/
public class EmvParser {
/**
* Class Logger
*/
private static final Logger LOGGER = LoggerFactory.getLogger(EmvParser.class);
/**
* PPSE directory "2PAY.SYS.DDF01"
*/
private static final byte[] PPSE = "2PAY.SYS.DDF01".getBytes();
/**
* PSE directory "1PAY.SYS.DDF01"
*/
private static final byte[] PSE = "1PAY.SYS.DDF01".getBytes();
/**
* Unknow response
*/
public static final int UNKNOW = -1;
/**
* Provider
*/
private IProvider provider;
/**
* use contact less mode
*/
private boolean contactLess;
/**
* Constructor
*
* @param pProvider
* provider to launch command
* @param pContactLess
* boolean to indicate if the EMV card is contact less or not
*/
public EmvParser(final IProvider pProvider, final boolean pContactLess) {
provider = pProvider;
contactLess = pContactLess;
}
/**
* Method used to read public data from EMV card
*
* @return data read from card or null if any provider match the card type
*/
public EmvCard readEmvCard() throws CommunicationException {
// use PSE first
EmvCard card = readWithPSE();
// Find with AID
if (card == null) {
card = readWithAID();
}
return card;
}
/**
* Method used to select payment environment PSE or PPSE
*
* @return response byte array
* @throws CommunicationException
*/
protected byte[] selectPaymentEnvironment() throws CommunicationException {
if (LOGGER.isDebugEnabled()) {
LOGGER.info("Select " + (contactLess ? "PPSE" : "PSE") + " Application");
}
// Select the PPSE or PSE directory
return provider.transceive(new CommandApdu(CommandEnum.SELECT, contactLess ? PPSE : PSE, 0).toBytes());
}
/**
* Method used to get the number of pin try left
*
* @return the number of pin try left
* @throws CommunicationException
*/
protected int getLeftPinTry() throws CommunicationException {
int ret = UNKNOW;
if (LOGGER.isDebugEnabled()) {
LOGGER.info("Get Left PIN try");
}
// Left PIN try command
byte[] data = provider.transceive(new CommandApdu(CommandEnum.GET_DATA, 0x9F, 0x17, 0).toBytes());
if (ResponseUtils.isSucceed(data)) {
// Extract PIN try counter
byte[] val = TlvUtil.getValue(data, EmvTags.PIN_TRY_COUNTER);
if (val != null) {
ret = BytesUtils.byteArrayToInt(val);
}
}
return ret;
}
/**
* Method used to parse FCI Proprietary Template
*
* @param pData
* data to parse
* @return
* @throws CommunicationException
*/
protected byte[] parseFCIProprietaryTemplate(final byte[] pData) throws CommunicationException {
// Get SFI
byte[] data = TlvUtil.getValue(pData, EmvTags.SFI);
// Check SFI
if (data != null) {
int sfi = BytesUtils.byteArrayToInt(data);
if (LOGGER.isDebugEnabled()) {
LOGGER.info("SFI found:" + sfi);
}
data = provider.transceive(new CommandApdu(CommandEnum.READ_RECORD, sfi, sfi << 3 | 4, 0).toBytes());
// If LE is not correct
if (ResponseUtils.isEquals(data, SwEnum.SW_6C)) {
data = provider.transceive(new CommandApdu(CommandEnum.READ_RECORD, sfi, sfi << 3 | 4, data[data.length - 1])
.toBytes());
}
return data;
}
if (LOGGER.isDebugEnabled()) {
LOGGER.info("(FCI) Issuer Discretionary Data is already present");
}
return pData;
}
/**
* Method used to extract application label
*
* @return decoded application label or null
*/
protected String extractApplicationLabel(final byte[] pData) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Extract Application label");
}
String label = null;
byte[] labelByte = TlvUtil.getValue(pData, EmvTags.APPLICATION_LABEL);
if (labelByte != null) {
label = new String(labelByte);
}
return label;
}
/**
* Read EMV card with Payment System Environment or Proximity Payment System Environment
*
* @return Read card
*/
protected EmvCard readWithPSE() throws CommunicationException {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Try to read card with Payment System Environment");
}
EmvCard card = null;
// Select the PPSE or PSE directory
byte[] data = selectPaymentEnvironment();
if (ResponseUtils.isSucceed(data)) {
// Parse FCI Template
data = parseFCIProprietaryTemplate(data);
// Extract application label
if (ResponseUtils.isSucceed(data)) {
String label = extractApplicationLabel(data);
// Get Card
card = extractPublicData(TlvUtil.getValue(data, EmvTags.AID_CARD), label);
}
} else if (LOGGER.isDebugEnabled()) {
LOGGER.debug((contactLess ? "PPSE" : "PSE") + " not found -> Use kown AID");
}
return card;
}
/**
* Read EMV card with AID
*
* @return Card read
*/
protected EmvCard readWithAID() throws CommunicationException {
EmvCard card = null;
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Try to read card with AID");
}
// Test each card from know EMV AID
for (EmvCardScheme type : EmvCardScheme.values()) {
for (byte[] aid : type.getAidByte()) {
card = extractPublicData(aid, type.getName());
if (card != null) {
break;
}
}
if (card != null) {
break;
}
}
return card;
}
/**
* Select application with AID or RID
*
* @param pAid
* byte array containing AID or RID
* @return response byte array
* @throws CommunicationException
*/
protected byte[] selectAID(final byte[] pAid) throws CommunicationException {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Select AID: " + BytesUtils.bytesToString(pAid));
}
return provider.transceive(new CommandApdu(CommandEnum.SELECT, pAid, 0).toBytes());
}
/**
* Read public card data from parameter AID
*
* @param pAid
* card AID in bytes
* @param pApplicationLabel
* application scheme (Application label)
* @return card read or null
*/
protected EmvCard extractPublicData(final byte[] pAid, final String pApplicationLabel) throws CommunicationException {
EmvCard ret = null;
// Select AID
byte[] data = selectAID(pAid);
// check response
if (ResponseUtils.isSucceed(data)) {
// Parse select response
ret = parse(data, provider);
if (ret != null) {
// Get AID
String aid = BytesUtils.bytesToStringNoSpace(TlvUtil.getValue(data, EmvTags.DEDICATED_FILE_NAME));
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Application label:" + pApplicationLabel + " with Aid:" + aid);
}
ret.setAid(aid);
ret.setType(findCardScheme(aid, ret.getCardNumber()));
ret.setApplicationLabel(pApplicationLabel);
ret.setLeftPinTry(getLeftPinTry());
}
}
return ret;
}
/**
* Method used to find the real card scheme
*
* @param pAid
* card complete AID
* @param pCardNumber
* card number
* @return card scheme
*/
protected EmvCardScheme findCardScheme(final String pAid, final String pCardNumber) {
EmvCardScheme type = EmvCardScheme.getCardTypeByAid(pAid);
// Get real type for french card
if (type == EmvCardScheme.CB) {
type = EmvCardScheme.getCardTypeByCardNumber(pCardNumber);
if (type != null) {
LOGGER.info("Real type:" + type.getName());
}
}
return type;
}
/**
* Method used to extract Log Entry from Select response
*
* @param pSelectResponse
* select response
* @return byte array
*/
protected byte[] getLogEntry(final byte[] pSelectResponse) {
return TlvUtil.getValue(pSelectResponse, EmvTags.LOG_ENTRY, EmvTags.VISA_LOG_ENTRY);
}
/**
* Method used to parse EMV card
*/
protected EmvCard parse(final byte[] pSelectResponse, final IProvider pProvider) throws CommunicationException {
EmvCard card = null;
// Get TLV log entry
byte[] logEntry = getLogEntry(pSelectResponse);
// Get PDOL
byte[] pdol = TlvUtil.getValue(pSelectResponse, EmvTags.PDOL);
// Send GPO Command
byte[] gpo = getGetProcessingOptions(pdol, pProvider);
// Check empty PDOL
if (!ResponseUtils.isSucceed(gpo)) {
gpo = getGetProcessingOptions(null, pProvider);
// Check response
if (!ResponseUtils.isSucceed(gpo)) {
return null;
}
}
// Extract commons card data (number, expire date, ...)
card = extractCommonsCardData(gpo);
// Extract log entry
if (card != null) {
card.setListTransactions(extractLogEntry(logEntry));
}
return card;
}
/**
* Method used to extract commons card data
*
* @param pCard
* Card data
* @param pGpo
* global processing options response
*/
protected EmvCard extractCommonsCardData(final byte[] pGpo) throws CommunicationException {
EmvCard card = null;
// Extract data from Message Template 1
byte data[] = TlvUtil.getValue(pGpo, EmvTags.RESPONSE_MESSAGE_TEMPLATE_1);
if (data != null) {
data = ArrayUtils.subarray(data, 2, data.length);
} else { // Extract AFL data from Message template 2
card = extractTrack2Data(pGpo);
if (card == null) {
data = TlvUtil.getValue(pGpo, EmvTags.APPLICATION_FILE_LOCATOR);
}
}
if (data != null) {
// Get SFI
List listAfl = extractAfl(data);
// for each AFL
for (Afl afl : listAfl) {
// check all records
for (int index = afl.getFirstRecord(); index <= afl.getLastRecord(); index++) {
byte[] info = provider.transceive(new CommandApdu(CommandEnum.READ_RECORD, index, afl.getSfi() << 3 | 4, 0)
.toBytes());
if (ResponseUtils.isEquals(info, SwEnum.SW_6C)) {
info = provider.transceive(new CommandApdu(CommandEnum.READ_RECORD, index, afl.getSfi() << 3 | 4,
info[info.length - 1]).toBytes());
}
// Extract card data
if (ResponseUtils.isSucceed(info)) {
card = extractTrack2Data(info);
if (card != null) {
break;
}
}
}
if (card != null) {
break;
}
}
}
return card;
}
/**
* Method used to get log format
*
* @return list of tag and length for the log format
* @throws CommunicationException
*/
protected List getLogFormat() throws CommunicationException {
List ret = new ArrayList();
if (LOGGER.isDebugEnabled()) {
LOGGER.info("GET log format");
}
// Get log format
byte[] data = provider.transceive(new CommandApdu(CommandEnum.GET_DATA, 0x9F, 0x4F, 0).toBytes());
if (ResponseUtils.isSucceed(data)) {
ret = TlvUtil.parseTagAndLength(TlvUtil.getValue(data, EmvTags.LOG_FORMAT));
}
return ret;
}
/**
* Method used to extract log entry from card
*
* @param pLogEntry
* log entry position
*/
protected List extractLogEntry(final byte[] pLogEntry) throws CommunicationException {
List listRecord = new ArrayList();
// If log entry is defined
if (pLogEntry != null) {
List tals = getLogFormat();
// read all records
for (int rec = 1; rec <= pLogEntry[1]; rec++) {
byte[] response = provider.transceive(new CommandApdu(CommandEnum.READ_RECORD, rec, pLogEntry[0] << 3 | 4, 0)
.toBytes());
// Extract data
if (ResponseUtils.isSucceed(response)) {
EmvTransactionRecord record = new EmvTransactionRecord();
record.parse(response, tals);
// Skip transaction with nul amount
if (record.getAmount() == null || record.getAmount() == 0) {
continue;
}
if (record != null) {
// Unknown currency
if (record.getCurrency() == null) {
record.setCurrency(CurrencyEnum.XXX);
}
listRecord.add(record);
}
} else if (SwEnum.getSW(response) == SwEnum.SW_6A83) { // No more transaction log
break;
}
}
}
return listRecord;
}
/**
* Extract list of application file locator from Afl response
*
* @param pAfl
* AFL data
* @return list of AFL
*/
protected List extractAfl(final byte[] pAfl) {
List list = new ArrayList();
ByteArrayInputStream bai = new ByteArrayInputStream(pAfl);
while (bai.available() >= 4) {
Afl afl = new Afl();
afl.setSfi(bai.read() >> 3);
afl.setFirstRecord(bai.read());
afl.setLastRecord(bai.read());
afl.setOfflineAuthentication(bai.read() == 1);
list.add(afl);
}
return list;
}
/**
* Extract track 2 data (card number + expire date)
*
* @param pData
* data
*/
protected EmvCard extractTrack2Data(final byte[] pData) {
EmvCard card = null;
byte[] track2 = TlvUtil.getValue(pData, EmvTags.TRACK_2_EQV_DATA, EmvTags.TRACK2_DATA);
if (track2 != null) {
card = new EmvCard();
BitUtils bit = new BitUtils(track2);
// Read card number
card.setCardNumber(bit.getNextHexaString(64));
// Read expire date
SimpleDateFormat sdf = new SimpleDateFormat("'D'yyMM", Locale.getDefault());
try {
String date = bit.getNextHexaString(5 * 4);
// fit template "'D'yyMM"
if (date.length() > 5) {
date = date.substring(0, 5);
}
card.setExpireDate(DateUtils.truncate(sdf.parse(date), Calendar.MONTH));
} catch (ParseException e) {
LOGGER.error("Unparsable expire card date");
}
// Extract Card Holder name (if exist)
byte[] cardHolderByte = TlvUtil.getValue(pData, EmvTags.CARDHOLDER_NAME);
if (cardHolderByte != null) {
card.setHolderName(new String(cardHolderByte).trim());
}
}
return card;
}
/**
* Method used to create GPO command and execute it
*
* @param pPdol
* PDOL data
* @param pProvider
* provider
* @return return data
*/
protected byte[] getGetProcessingOptions(final byte[] pPdol, final IProvider pProvider) throws CommunicationException {
// List Tag and length from PDOL
List list = TlvUtil.parseTagAndLength(pPdol);
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
out.write(EmvTags.COMMAND_TEMPLATE.getTagBytes()); // COMMAND TEMPLATE
out.write(TlvUtil.getLength(list)); // ADD total length
if (list != null) {
for (TagAndLength tl : list) {
out.write(EmvTerminal.constructValue(tl));
}
}
} catch (IOException ioe) {
LOGGER.error("Construct GPO Command:" + ioe.getMessage(), ioe);
}
return pProvider.transceive(new CommandApdu(CommandEnum.GPO, out.toByteArray(), 0).toBytes());
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy