/*
* Copyright (C) 2013 MILLAU Julien
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.github.devnied.emvnfccard.parser;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
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.TLV;
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 com.github.devnied.emvnfccard.utils.TrackUtils;
import fr.devnied.bitlib.BytesUtils;
/**
* Emv Parser.<br/>
* 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;
/**
* Card holder name separator
*/
public static final String CARD_HOLDER_NAME_SEPARATOR = "/";
/**
* Provider
*/
private IProvider provider;
/**
* use contact less mode
*/
private boolean contactLess;
/**
* Card data
*/
private EmvCard card;
/**
* 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;
card = new EmvCard();
}
/**
* 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
if (!readWithPSE()) {
// Find with AID
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.debug("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.debug("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.debug("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.debug("(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 true is succeed false otherwise
*/
protected boolean readWithPSE() throws CommunicationException {
boolean ret = false;
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Try to read card with Payment System Environment");
}
// 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)) {
// Get Aids
List<byte[]> aids = getAids(data);
for (byte[] aid : aids) {
ret = extractPublicData(aid, extractApplicationLabel(data));
if (ret == true) {
break;
}
}
if (!ret) {
card.setNfcLocked(true);
}
}
} else if (LOGGER.isDebugEnabled()) {
LOGGER.debug((contactLess ? "PPSE" : "PSE") + " not found -> Use kown AID");
}
return ret;
}
/**
* Method used to get the aid list, if the Kernel Identifier is defined, <br/>
* this value need to be appended to the ADF Name in the data field of <br/>
* the SELECT command.
*
* @param pData
* FCI proprietary template data
* @return the Aid to select
*/
protected List<byte[]> getAids(final byte[] pData) {
List<byte[]> ret = new ArrayList<byte[]>();
List<TLV> listTlv = TlvUtil.getlistTLV(pData, EmvTags.AID_CARD, EmvTags.KERNEL_IDENTIFIER);
for (TLV tlv : listTlv) {
if (tlv.getTag() == EmvTags.KERNEL_IDENTIFIER && ret.size() != 0) {
ret.add(ArrayUtils.addAll(ret.get(ret.size() - 1), tlv.getValueBytes()));
} else {
ret.add(tlv.getValueBytes());
}
}
return ret;
}
/**
* Read EMV card with AID
*/
protected void readWithAID() throws CommunicationException {
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()) {
if (extractPublicData(aid, type.getName())) {
return;
}
}
}
}
/**
* 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 true if succeed false otherwise
*/
protected boolean extractPublicData(final byte[] pAid, final String pApplicationLabel) throws CommunicationException {
boolean ret = false;
// Select AID
byte[] data = selectAID(pAid);
// check response
if (ResponseUtils.isSucceed(data)) {
// Parse select response
ret = parse(data, provider);
if (ret) {
// Get AID
String aid = BytesUtils.bytesToStringNoSpace(TlvUtil.getValue(data, EmvTags.DEDICATED_FILE_NAME));
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Application label:" + pApplicationLabel + " with Aid:" + aid);
}
card.setAid(aid);
card.setType(findCardScheme(aid, card.getCardNumber()));
card.setApplicationLabel(pApplicationLabel);
card.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.debug("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 boolean parse(final byte[] pSelectResponse, final IProvider pProvider) throws CommunicationException {
boolean ret = false;
// 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 false;
}
}
// Extract commons card data (number, expire date, ...)
if (extractCommonsCardData(gpo)) {
// Extract log entry
card.setListTransactions(extractLogEntry(logEntry));
ret = true;
}
return ret;
}
/**
* Method used to extract commons card data
*
* @param pGpo
* global processing options response
*/
protected boolean extractCommonsCardData(final byte[] pGpo) throws CommunicationException {
boolean ret = false;
// 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
ret = TrackUtils.extractTrack2Data(card, pGpo);
if (!ret) {
data = TlvUtil.getValue(pGpo, EmvTags.APPLICATION_FILE_LOCATOR);
} else {
extractCardHolderName(pGpo);
}
}
if (data != null) {
// Extract Afl
List<Afl> 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)) {
extractCardHolderName(info);
if (TrackUtils.extractTrack2Data(card, info)) {
return true;
}
}
}
}
}
return ret;
}
/**
* Method used to get log format
*
* @return list of tag and length for the log format
* @throws CommunicationException
*/
protected List<TagAndLength> getLogFormat() throws CommunicationException {
List<TagAndLength> ret = new ArrayList<TagAndLength>();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("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<EmvTransactionRecord> extractLogEntry(final byte[] pLogEntry) throws CommunicationException {
List<EmvTransactionRecord> listRecord = new ArrayList<EmvTransactionRecord>();
// If log entry is defined
if (pLogEntry != null) {
List<TagAndLength> 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);
// Fix artifact in EMV VISA card
if (record.getAmount() >= 1500000000) {
record.setAmount(record.getAmount() - 1500000000);
}
// 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 {
// No more transaction log or transaction disabled
break;
}
}
}
return listRecord;
}
/**
* Extract list of application file locator from Afl response
*
* @param pAfl
* AFL data
* @return list of AFL
*/
protected List<Afl> extractAfl(final byte[] pAfl) {
List<Afl> list = new ArrayList<Afl>();
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 card holder lastname and firstname
*
* @param pData
* card data
*/
protected void extractCardHolderName(final byte[] pData) {
// Extract Card Holder name (if exist)
byte[] cardHolderByte = TlvUtil.getValue(pData, EmvTags.CARDHOLDER_NAME);
if (cardHolderByte != null) {
String[] name = StringUtils.split(new String(cardHolderByte).trim(), CARD_HOLDER_NAME_SEPARATOR);
if (name != null && name.length == 2) {
card.setHolderFirstname(StringUtils.trimToNull(name[0]));
card.setHolderLastname(StringUtils.trimToNull(name[1]));
}
}
}
/**
* 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<TagAndLength> 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());
}
/**
* Method used to get the field card
*
* @return the card
*/
public EmvCard getCard() {
return card;
}
}