com.github.devnied.emvnfccard.parser.EmvParser.java Source code

Java tutorial

Introduction

Here is the source code for com.github.devnied.emvnfccard.parser.EmvParser.java

Source

/*
 * 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;
    }

}