Android Open Source - bankomatinfos Emv Utils






From Project

Back to project page bankomatinfos.

License

The source code is released under:

GNU General Public License

If you think the Android project bankomatinfos listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

package at.zweng.bankomatinfos.iso7816emv;
//from w ww  .j av a  2  s.  c o  m
import static at.zweng.bankomatinfos.util.Utils.*;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;

import android.content.Context;
import android.util.Log;
import at.zweng.bankomatinfos.exceptions.TlvParsingException;
import at.zweng.bankomatinfos.model.InfoKeyValuePair;
import at.zweng.bankomatinfos2.R;

/**
 * Util functions around EMV (https://en.wikipedia.org/wiki/EMV) standard and
 * ISO 7816<br>
 * <br>
 * See here for some basic ISO 7816-4 command infos:
 * http://www.cardwerk.com/smartcards/smartcard_standard_ISO7816-
 * 4_6_basic_interindustry_commands.aspx#chap6_1
 * 
 * @author Johannes Zweng <johannes@zweng.at>
 */
public class EmvUtils {

  /**
   * ISO command for SELECT a file directly (Direct selection by DF name (data
   * field=DF name)
   */
  public static final byte[] ISO_COMMAND_SELECT_DIRECT = { (byte) 0x00,
      (byte) 0xA4, (byte) 0x04, (byte) 0x00 };

  /**
   * ISO command for SELECT PARENT DF file (empty data field)
   */
  public static final byte[] ISO_COMMAND_SELECT_PARENT_DF = { (byte) 0x00,
      (byte) 0xA4, (byte) 0x03, (byte) 0x00 };

  /**
   * ISO command for SELECT FILE by file identifier (data field=file
   * idenntifier)
   */
  public static final byte[] ISO_COMMAND_SELECT_FILE = { (byte) 0x00,
      (byte) 0xA4, (byte) 0x00, (byte) 0x00 };

  /**
   * command read QUICK balance: 00B0820000 (in fact this is a READ BINARY
   * command, reading EF 2 starting from offset 00, reading all bytes)
   */
  public static final byte[] ISO_COMMAND_QUICK_READ_BALANCE = { (byte) 0x00,
      (byte) 0xB0, (byte) 0x82, (byte) 0x00, (byte) 0x00 };

  /**
   * command read QUICK currency: 00B0810000 (in fact this is a READ BINARY
   * command, reading EF 1 starting from offset 0x15, reading 2 bytes)
   */
  public static final byte[] ISO_COMMAND_QUICK_READ_CURRENCY = { (byte) 0x00,
      (byte) 0xB0, (byte) 0x81, (byte) 0x15, (byte) 0x02 };

  /**
   * GET_CPLC_COMMAND command for receiving "Card Production Life Cycle"
   * (CPLC) data, according the GlobalPlatform Card Specification.
   * 
   */
  public static final byte[] GPCS_GET_CPLC_COMMAND = { (byte) 0x80,
      (byte) 0xCA, (byte) 0x9F, (byte) 0x7F, 0x00 };

  /**
   * GET_CPLC_COMMAND , but with Le set to 2D (some cards seem to require it
   * this way)
   */
  public static final byte[] GPCS_GET_CPLC_COMMAND_WITH_LENGTH = {
      (byte) 0x80, (byte) 0xCA, (byte) 0x9F, (byte) 0x7F, 0x2D };

  /**
   * EMV command GET CHALLENGE (returns 8 byte random number), used in
   * transactions.
   */
  // DANGEROUS!!!!!!
  // DANGEROUS!!!!!!
  // DANGEROUS!!!!!!
  //
  // GET CHALLENGE is an active command which changes the state in your
  // card! Only use if you know what you do!
  //
  public static final byte[] EMV_COMMAND_GET_CHALLENGE = { (byte) 0x00,
      (byte) 0x84, (byte) 0x00, (byte) 0x00, (byte) 0x00 };

  /**
   * EMV GET DATA command for reading Tag "ATC" (Tag 9F 36)
   */
  public static final byte[] EMV_COMMAND_GET_DATA_APP_TX_COUNTER = {
      (byte) 0x80, (byte) 0xCA, (byte) 0x9F, (byte) 0x36, (byte) 0x00 };

  /**
   * EMV GET DATA command for reading Tag C8 (card risk management country?)
   */
  public static final byte[] EMV_COMMAND_GET_DATA_CRM_COUNTRY = {
      (byte) 0x80, (byte) 0xCA, (byte) 0x00, (byte) 0xC8, (byte) 0x00 };

  /**
   * EMV GET DATA command for reading Tag C9 (card risk management currency)
   */
  public static final byte[] EMV_COMMAND_GET_DATA_CRM_CURRENCY = {
      (byte) 0x80, (byte) 0xCA, (byte) 0x00, (byte) 0xC9, (byte) 0x00 };

  /**
   * EMV GET DATA command for reading Tag 9F14 (Lower Consecutive Offline
   * Limit?)
   */
  public static final byte[] EMV_COMMAND_GET_DATA_LOWER_CONSECUTIVE_OFFLINE_LIMIT = {
      (byte) 0x80, (byte) 0xCA, (byte) 0x9F, (byte) 0x14, (byte) 0x00 };

  /**
   * EMV GET DATA command for reading Tag 9F23 (Upper Consecutive Offline
   * Limit?)
   */
  public static final byte[] EMV_COMMAND_GET_DATA_UPPER_CONSECUTIVE_OFFLINE_LIMIT = {
      (byte) 0x80, (byte) 0xCA, (byte) 0x9F, (byte) 0x23, (byte) 0x00 };

  /**
   * EMV GET DATA command for reading Tag CA (Lower Cumulative Offline
   * Transaction Amount?)
   */
  public static final byte[] EMV_COMMAND_GET_DATA_LOWER_CUMULATIVE_TX_AMOUNT = {
      (byte) 0x80, (byte) 0xCA, (byte) 0x00, (byte) 0xCA, (byte) 0x00 };

  /**
   * EMV GET DATA command for reading Tag CB (Upper Cumulative Offline
   * Transaction Amount?)
   */
  public static final byte[] EMV_COMMAND_GET_DATA_UPPER_CUMULATIVE_TX_AMOUNT = {
      (byte) 0x80, (byte) 0xCA, (byte) 0x00, (byte) 0xCB, (byte) 0x00 };

  /**
   * EMV GET DATA command for reading Tag "Last Online ATC Register" (Tag 9F
   * 13)
   */
  public static final byte[] EMV_COMMAND_GET_DATA_LAST_ONLINE_APP_TX_COUNTER = {
      (byte) 0x80, (byte) 0xCA, (byte) 0x9F, (byte) 0x13, (byte) 0x00 };

  /**
   * EMV GET DATA command for reading Tag "PIN retry counter" (Tag 9F 17)
   */
  public static final byte[] EMV_COMMAND_GET_DATA_PIN_RETRY_COUNTER = {
      (byte) 0x80, (byte) 0xCA, (byte) 0x9F, (byte) 0x17, (byte) 0x00 };

  /**
   * EMV GET DATA command for reading "accumulator values and limits" (BF 30)
   * 
   * @see "EMV Common Payment Application Specification v1 Dec 2005.pdf",
   *      p.155f
   */
  // TODO: test this command
  public static final byte[] EMV_COMMAND_GET_DATA_ACCUMULATOR_VALUES = {
      (byte) 0x80, (byte) 0xCA, (byte) 0xBF, (byte) 0x30, (byte) 0x00 };

  /**
   * EMV GET DATA command for reading "counter values and limits" (BF 35)
   * 
   * @see "EMV Common Payment Application Specification v1 Dec 2005.pdf",
   *      p.155f
   */
  // TODO: test this command
  public static final byte[] EMV_COMMAND_GET_DATA_COUNTER_VALUES = {
      (byte) 0x80, (byte) 0xCA, (byte) 0xBF, (byte) 0x35, (byte) 0x00 };

  /**
   * EMV GET DATA command for reading "offline balance" (9F 50)
   * 
   * @see "EMV Common Payment Application Specification v1 Dec 2005.pdf",
   *      p.155f
   */
  // TODO: test this command
  public static final byte[] EMV_COMMAND_GET_DATA_OFFLINE_BALANCE = {
      (byte) 0x80, (byte) 0xCA, (byte) 0x9F, (byte) 0x50, (byte) 0x00 };

  /**
   * EMV GET DATA command for reading Tag "Log format" (Tag 9F 4F)
   */
  public static final byte[] EMV_COMMAND_GET_DATA_LOG_FORMAT = { (byte) 0x80,
      (byte) 0xCA, (byte) 0x9F, (byte) 0x4F, (byte) 0x00 };

  /**
   * EMV command for GET DATA
   * "all the common BER-TLV data objects readable in the context" -->
   * http://www.cardwerk.com/smartcards/smartcard_standard_ISO7816-
   * 4_6_basic_interindustry_commands.aspx#chap6_9 , Table 52
   */
  public static final byte[] EMV_COMMAND_GET_DATA_ALL_COMMON_BER_TLV = {
      (byte) 0x80, (byte) 0xCA, (byte) 0x00, (byte) 0xFF, (byte) 0x00 };
  /**
   * EMV command for GET DATA
   * "all the common SIMPLE-TLV data objects readable in the context" -->
   * http://www.cardwerk.com/smartcards/smartcard_standard_ISO7816-
   * 4_6_basic_interindustry_commands.aspx#chap6_9 , Table 52
   */
  public static final byte[] EMV_COMMAND_GET_DATA_ALL_COMMON_SIMPLE_TLV = {
      (byte) 0x80, (byte) 0xCA, (byte) 0x02, (byte) 0xFF, (byte) 0x00 };

  /**
   * Application ID for Quick (IEP): D040000001000002
   */
  public static final byte[] APPLICATION_ID_QUICK = { (byte) 0xD0,
      (byte) 0x40, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00,
      (byte) 0x00, (byte) 0x02 };
  /**
   * Application ID for EMV Maestro Debit (Bankomat-Karte): A0000000043060
   */
  public static final byte[] APPLICATION_ID_EMV_MAESTRO_BANKOMAT = {
      (byte) 0xA0, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x04,
      (byte) 0x30, (byte) 0x60 };

  /**
   * Application ID for MASTERCARD: A0000000041010
   */
  public static final byte[] APPLICATION_ID_EMV_MASTERCARD = { (byte) 0xA0,
      (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x04, (byte) 0x10,
      (byte) 0x10 };

  /**
   * Application ID for Visa credit or debit card: A0000000031010
   */
  public static final byte[] APPLICATION_ID_EMV_VISA_CREDITCARD = {
      (byte) 0xA0, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x03,
      (byte) 0x10, (byte) 0x10 };

  //
  // Values of the status word (last 2 bytes) in the response
  //
  public static final byte[] SW_SUCCESS = { (byte) 0x90, (byte) 0x00 };
  public static final byte[] SW_DATA_FAILURE = { (byte) 0x62, (byte) 0x81 };
  public static final byte[] SW_FILEEND_REACHED = { (byte) 0x62, (byte) 0x82 };
  public static final byte[] SW_FILE_LOCKED = { (byte) 0x62, (byte) 0x83 };
  public static final byte[] SW_FILEINFO_ISO_FAILURE = { (byte) 0x62,
      (byte) 0x84 };
  public static final byte[] SW_MEMORY_ERROR = { (byte) 0x65, (byte) 0x81 };
  public static final byte[] SW_LENGTH_ERROR = { (byte) 0x67, (byte) 0x00 };
  public static final byte[] SW_FUNC_CLASS_BYTE_NOT_SUPPORTED = {
      (byte) 0x68, (byte) 0x00 };
  public static final byte[] SW_LOGIC_CHAN_NOT_SUPPORTED = { (byte) 0x68,
      (byte) 0x81 };
  public static final byte[] SW_SEC_MSG_NOT_SUPPORTED = { (byte) 0x68,
      (byte) 0x82 };
  public static final byte[] SW_CMD_NOT_ALLOWED = { (byte) 0x69, (byte) 0x00 };
  public static final byte[] SW_CMD_INCOMPATIBLE = { (byte) 0x69, (byte) 0x81 };
  public static final byte[] SW_SEC_STATE_NOT_FULFILLED = { (byte) 0x69,
      (byte) 0x82 };
  public static final byte[] SW_AUTH_METHOD_LOCKED = { (byte) 0x69,
      (byte) 0x83 };
  public static final byte[] SW_REFERENCED_DATA_LOCKED = { (byte) 0x69,
      (byte) 0x84 };
  public static final byte[] SW_USAGE_COND_NOT_FULFILLED = { (byte) 0x69,
      (byte) 0x85 };
  public static final byte[] SW_CMD_NOT_ALLOWED_NO_EF_SEL = { (byte) 0x69,
      (byte) 0x86 };
  // einige ausgelassen
  public static final byte[] SW_INCORRECT_PARAMS = { (byte) 0x6A, (byte) 0x00 };
  public static final byte[] SW_INCORRECT_DATA = { (byte) 0x6A, (byte) 0x80 };
  public static final byte[] SW_FUNC_NOT_SUPPORTED = { (byte) 0x6A,
      (byte) 0x81 };
  public static final byte[] SW_FILE_NOT_FOUND = { (byte) 0x6A, (byte) 0x82 };
  public static final byte[] SW_RECORD_NOT_FOUND = { (byte) 0x6A, (byte) 0x83 };
  public static final byte[] SW_REFERENCED_DATA_NOT_FOUND = { (byte) 0x6A,
      (byte) 0x88 };
  public static final byte[] SW_INCORRECT_PARAMETERS_P1_P2 = { (byte) 0x6A,
      (byte) 0x86 };
  // ..
  public static final byte[] SW_CMD_CLASS_NOT_SUPPORTED = { (byte) 0x6E,
      (byte) 0x00 };
  public static final byte[] SW_CMD_ABORTED_UNKNOWN_ERR = { (byte) 0x6F,
      (byte) 0x00 };
  public static final byte[] SW_INS_NOT_SUPPORTED = { (byte) 0x6D,
      (byte) 0x00 };
  public static final byte[] SW_COMMAND_NOT_ALLOWED = { (byte) 0x69,
      (byte) 0x86 };
  public static final short SW_APPLET_SELECT_FAILED = 0x6999;
  public static final short SW_CLA_NOT_SUPPORTED = 0x6E00;
  public static final short SW_SECURITY_STATUS_NOT_SATISFIED = 0x6982;
  public static final short SW_DATA_INVALID = 0x6984;
  public static final short SW_CONDITIONS_NOT_SATISFIED = 0x6985;
  public static final short SW_WRONG_LENGTH = 0x6700;
  public static final short SW_WRONG_DATA = 0x6A80;
  public static final short SW_WRONG_P1P2 = 0x6B00;
  public static final short SW_UNKNOWN = 0x6F00;

  /**
   * Creates a APDU to send
   * 
   * @param command
   * @param cmdData
   * @param lengthExpected
   *            the expected result length
   * @return
   */
  public static byte[] createAPDU(byte[] command, byte[] cmdData,
      byte lengthExpected) {
    // Log.d(TAG, "createAPDU command length: " + command.length);
    // Log.d(TAG, "createAPDU cmdData length: " + cmdData.length);

    byte[] pdu;
    // length of new PDU is command + data + 2
    // length of new PDU is command + 1 if data is 0-length
    if (cmdData.length > 0) {
      pdu = new byte[command.length + cmdData.length + 2];
    } else {
      pdu = new byte[command.length + 1];
    }

    // Log.d(TAG, "createAPDU pdu toal length: " + pdu.length);

    // the first 5 bytes (or shorter) seem to contain the command
    System.arraycopy(command, 0, pdu, 0, command.length);

    if (cmdData.length > 0) {
      // the 5th byte contains the length of the data section (1-byte)
      pdu[command.length] = ((byte) cmdData.length);

      // then starting at offset 5 the data is copied in
      System.arraycopy(cmdData, 0, pdu, 5, cmdData.length);
    }

    // because the total length is length of command + length of data + 2,
    // we will still have 1 place left at the end of the array. --> LE field
    pdu[pdu.length - 1] = lengthExpected;

    //
    // Example:
    // command: 00 A4 04 00 (length 4)
    // data: D0 40 00 00 01 00 00 02 (length 8)
    // --> will result in:
    // PDU: 00 A4 04 00 08 D0 40 00 00 01 00 00 02 00 (length 14)

    // Log.d(TAG, "createAPDU returns: " + bytesToHex(pdu));
    return pdu;
  }

  /**
   * Creates a SELECT command PDU
   * 
   * @param appId
   * @return
   */
  public static byte[] createSelectAid(byte[] appId) {
    byte[] result = createAPDU(ISO_COMMAND_SELECT_DIRECT, appId, (byte) 0);
    return result;
  }

  /**
   * Creates a SELECT MF (master file) command PDU (aka: switch to "root dir")
   * 
   * @return
   */
  public static byte[] createSelectMasterFile() {
    byte[] result = createAPDU(ISO_COMMAND_SELECT_FILE, new byte[0],
        (byte) 0);
    return result;
  }

  /**
   * Creates a SELECT FILE command PDU (selects file by file identifier bytes)
   * 
   * @param fileIdentifier
   *            identifier bytes
   * @return
   */
  public static byte[] createSelectFile(byte[] fileIdentifier) {
    byte[] result = createAPDU(ISO_COMMAND_SELECT_FILE, fileIdentifier,
        (byte) 0);
    return result;
  }

  /**
   * Creates a SELECT PARENT DF FILE command PDU (aka: cd ..)
   * 
   * @return
   */
  public static byte[] createSelectParentDfFile() {
    byte[] result = createAPDU(ISO_COMMAND_SELECT_PARENT_DF, new byte[0],
        (byte) 0);
    return result;
  }

  /**
   * Creates a READ BINARY command by short EF identifier PDU<br>
   * See http://www.cardwerk.com/smartcards/smartcard_standard_ISO7816-
   * 4_6_basic_interindustry_commands.aspx#chap6_1 for details
   * 
   * @param shortEfFileIdentifier
   *            short id of the EF to read
   * @param offset
   *            offset within the file
   * @return command APDU
   */
  public static byte[] createReadBinaryApdu(int shortEfFileIdentifier,
      int offset) {
    int sfi = shortEfFileIdentifier;
    StringBuilder cmd = new StringBuilder();
    cmd.append("00B0");

    if (shortEfFileIdentifier > 7 || shortEfFileIdentifier < 0) {
      throw new IllegalArgumentException(
          "createReadBinaryApdu: shortEfFileIdentifier canonly contain values from 0 to 7. We got: "
              + shortEfFileIdentifier);
    }

    // "If bit8=1 in P1, then bit7-6 are set to 0. bit3-1 of P1 are a short
    // EF (Elementary File) identifier and P2 is the offset of the first
    // byte to be read in date units from the beginning of the file."

    // P1:
    sfi = sfi | 0x80;
    cmd.append(int2Hex(sfi));

    // P2: offset
    cmd.append(int2Hex(offset));
    // and we set the LE field to 00:
    cmd.append("00");
    return fromHexString(cmd.toString());
  }

  /**
   * Creates a READ RECORD command PDU<br>
   * See http://www.cardwerk.com/smartcards/smartcard_standard_ISO7816-
   * 4_6_basic_interindustry_commands.aspx#chap6_5 for details
   * 
   * @param shortEfFileIdentifier
   * @param record
   * @return command APDU
   */
  public static byte[] createReadRecordApdu(int shortEfFileIdentifier,
      int record) {
    int sfi = shortEfFileIdentifier;
    StringBuilder cmd = new StringBuilder();
    cmd.append("00B2");
    // P1 byte:
    cmd.append(int2Hex(record));
    // P2 byte:
    // b8 b7 b6 b5 b4 b3 b2 b1
    // b8-b4 contain the SFI
    sfi = sfi << 3;
    // and we set the right 3 bits (b1-b3) to 100
    sfi += 4;
    cmd.append(int2Hex(sfi));
    // and we set the LE field to 00:
    cmd.append("00");
    return fromHexString(cmd.toString());
  }

  /**
   * Calculates a (hopefully) correct APDU for the EMV GET PROCESSING OPTIONS
   * command, based on the result of the select application response of the
   * card.
   * 
   * @param selectionResponse
   * @return
   */
  public static byte[] createGetProcessingOptionsApdu(byte[] selectionResponse) {
    // TODO implement createGetProcessingOptionsApdu!!

    // This method currently returns a static value, which I manually
    // computed based on the data of my test card. May NOT WORK with other
    // cards which send different PDOL
    // IMPLEMENT ME!!

    // In short:
    // ----------
    // When selecting an application the card includes in its response the
    // tag 9F38 "Processing Options Data Object List (PDOL)" (see also
    // http://www.eftlab.co.uk/index.php/site-map/knowledge-base/145-emv-nfc-tags)
    //
    // The card declares with the PDOL a number of tags and their expected
    // lengths which it wants to see in a following GET PROCESSING OPTIONS
    // command.
    //
    // EXAMPLE:
    // My card returns a PDOL of "9f 5c 08".
    // This contains only a single tag (9f 5c) with a length value of 08.
    // "9f 5c" is the tag "Cumulative Total Transaction Amount Upper Limit"
    // (CTTAUL)". So my card only wants to get 8 bytes in the GET PROCESSING
    // OPTIONS which represent the CTTAUL value.
    // EMV cards may also request more than 1 tag, for example terminal's
    // country or currency code.. or a random number.. etc.

    //
    // Manually building my GET PROCESSING OPTIONS command:
    //

    // "9f 5c 08" = PDOL as returned on app selection in Tag 9f38

    // Manually constructed command:
    // 80A80000 0A 83 08 FFFFFFFFFFFFFFFF 00
    // In detail:
    // 80A80000 is GET PROCESSING OPTIONS
    // 0A is total length of following PDOL (=11 bytes)
    // 83 is Tag "command template" and "Identifies the data field of a
    // command message"
    // 08 is the length of the following data
    // FFFFFFFFFFFFFFFF is the data (CTTAUL value in my case)
    // 00 is expected response length (unlimited)

    // return fromHexString("80A800000A8308000000000000000000");
    return fromHexString("80A800000A8308FFFFFFFFFFFFFFFF00");
  }

  /**
   * The VERIFY command is used for OFFLINE authentication. The Transaction
   * PIN Data (input) is compared with the Reference PIN Data stored in the
   * application (ICC).
   * 
   * NOTE: The EMV command "Offline PIN" is vulnerable to a Man-in-the-middle
   * attack. Terminals should request online pin verification instead!!
   * 
   * 
   * Case 3 C-APDU
   * 
   * @param pin
   *            the PIN to verify
   * @param transmitInPlaintext
   * @throws NumberFormatException
   *             if PIN cannot be parsed as integer
   * @return
   */
  public static byte[] createApduVerifyPIN(String pin,
      boolean transmitInPlaintext) throws NumberFormatException {
    int pinLength = pin.length();
    if (pinLength < 4 || pinLength > 12) { // 0x0C
      throw new IllegalArgumentException(
          "Invalid PIN length. Must be in the range 4 to 12. Length="
              + pinLength);
    }
    StringBuilder builder = new StringBuilder("00 20 00 ");

    // EMV book 3 Table 23 (page 88) lists 7 qualifiers,
    // but only 2 are relevant in our case (hence the use of boolean)
    byte p2QualifierPlaintextPIN = (byte) 0x80;
    byte p2QualifierEncipheredPIN = (byte) 0x88;
    if (transmitInPlaintext) {
      builder.append(byte2Hex(p2QualifierPlaintextPIN));
      byte[] tmp = new byte[8]; // Plaintext Offline PIN Block. This block
                    // is split into nibbles (4 bits)
      tmp[0] = (byte) 0x20; // Control field (binary 0010xxxx)
      tmp[0] |= pinLength;
      Arrays.fill(tmp, 1, tmp.length, (byte) 0xFF); // Filler bytes

      boolean highNibble = true; // Alternate between high and low nibble
      for (int i = 0; i < pinLength; i++) { // Put each PIN digit into its
                          // own nibble
        int pos = i / 2;
        int digit = Integer.parseInt(pin.substring(i, i + 1));
        if (highNibble) {
          tmp[1 + pos] &= (byte) 0x0F; // Clear bits
          tmp[1 + pos] |= (byte) (digit << 4);

        } else {
          tmp[1 + pos] &= (byte) 0xF0; // Clear bits
          tmp[1 + pos] |= (byte) (digit);
        }
        highNibble = !highNibble;
      }
      builder.append(" 08 "); // Lc length
      builder.append(bytesToHex(tmp)); // block
    } else {
      builder.append(byte2Hex(p2QualifierEncipheredPIN));
      // TODO Enciphered PIN not supported
      throw new UnsupportedOperationException(
          "Enciphered PIN not implemented");
    }
    return fromHexString(builder.toString());
  }

  /**
   * Check if the given 2 bytes status words mean SUCCESS
   * 
   * @param statusWord
   * @return string representation of the statusword
   */
  public static boolean isStatusSuccess(byte[] statusWord) {
    if (statusWord == null || statusWord.length != 2) {
      throw new IllegalArgumentException(
          "isStatusSuccess: status word was either null or length was != 2");
    }
    return compare2byteArrays(statusWord, SW_SUCCESS);
  }

  /**
   * @param statusWord
   * @return string representation of the statusword
   */
  public static String statusToString(byte[] statusWord) {
    if (statusWord == null || statusWord.length != 2) {
      throw new IllegalArgumentException(
          "checkStatusWord: status word was either null or length was != 2");
    }

    if (compare2byteArrays(statusWord, SW_SUCCESS)) {
      return "success :-)";
    } else if (statusWord[0] == (byte) 0x61) {
      return "success :-) response can be fetched by GET RESPONSE!";
    } else if (compare2byteArrays(statusWord, SW_DATA_FAILURE)) {
      return "returned data may be not correct";
    } else if (compare2byteArrays(statusWord, SW_FILEEND_REACHED)) {
      return "file end reached, could not read LE bytes";
    } else if (compare2byteArrays(statusWord, SW_FILE_LOCKED)) {
      return "file is locked";
    } else if (compare2byteArrays(statusWord, SW_FILEINFO_ISO_FAILURE)) {
      return "file info FCI is not ISO conform";
    } else if (statusWord[0] == (byte) 0x62) {
      return "warning: state of memory not changed";
    } else if (statusWord[0] == (byte) 0x63) {
      return "warning: state of memory not changed 2";
    } else if (statusWord[0] == (byte) 0x64) {
      return "warning: execution error";
    } else if (compare2byteArrays(statusWord, SW_MEMORY_ERROR)) {
      return "memory error";
    } else if (statusWord[0] == (byte) 0x65) {
      return "warning: execution error";
    } else if (compare2byteArrays(statusWord, SW_LENGTH_ERROR)) {
      return "length error, lc or le incorrect";
    } else if (compare2byteArrays(statusWord,
        SW_FUNC_CLASS_BYTE_NOT_SUPPORTED)) {
      return "function in class byte not supported";
    } else if (compare2byteArrays(statusWord, SW_LOGIC_CHAN_NOT_SUPPORTED)) {
      return "logical channels not supported";
    } else if (compare2byteArrays(statusWord, SW_SEC_MSG_NOT_SUPPORTED)) {
      return "secure messaging not supported";
    } else if (compare2byteArrays(statusWord, SW_CMD_NOT_ALLOWED)) {
      return "command not allowed";
    } else if (compare2byteArrays(statusWord, SW_CMD_INCOMPATIBLE)) {
      return "command incompatible with file system structure";
    } else if (compare2byteArrays(statusWord, SW_SEC_STATE_NOT_FULFILLED)) {
      return "security state not fulfilled";
    } else if (compare2byteArrays(statusWord, SW_AUTH_METHOD_LOCKED)) {
      return "authentication method is locked";
    } else if (compare2byteArrays(statusWord, SW_REFERENCED_DATA_LOCKED)) {
      return "referenced data is locked";
    } else if (compare2byteArrays(statusWord, SW_USAGE_COND_NOT_FULFILLED)) {
      return "usage conditions are not fulfilled";
    } else if (compare2byteArrays(statusWord, SW_CMD_NOT_ALLOWED_NO_EF_SEL)) {
      return "command not allowed (no EF selected)";
    } else if (compare2byteArrays(statusWord, SW_INCORRECT_PARAMS)) {
      return "incorrect parameters P1/P2";
    } else if (compare2byteArrays(statusWord, SW_INCORRECT_DATA)) {
      return "incorrect data for command";
    } else if (compare2byteArrays(statusWord, SW_FUNC_NOT_SUPPORTED)) {
      return "function is not supported";
    } else if (compare2byteArrays(statusWord, SW_FILE_NOT_FOUND)) {
      return "file not found";
    } else if (compare2byteArrays(statusWord, SW_RECORD_NOT_FOUND)) {
      return "record not found";
    } else if (compare2byteArrays(statusWord, SW_REFERENCED_DATA_NOT_FOUND)) {
      return "referenced data (data objects) not found";
    } else if (compare2byteArrays(statusWord, SW_INCORRECT_PARAMETERS_P1_P2)) {
      return "incorrect parameters p1/p2";
    } else if (statusWord[0] == (byte) 0x6C) {
      return "incorrect length, second byte specifies correct length";
    } else if (compare2byteArrays(statusWord, SW_DATA_FAILURE)) {
      return "success";
    } else if (compare2byteArrays(statusWord, SW_CMD_CLASS_NOT_SUPPORTED)) {
      return "this command class (CLA) is not supported";
    } else if (compare2byteArrays(statusWord, SW_CMD_ABORTED_UNKNOWN_ERR)) {
      return "command aborted with unknown errors";
    } else if (compare2byteArrays(statusWord, SW_INS_NOT_SUPPORTED)) {
      return "instruction not supported";
    } else if (compare2byteArrays(statusWord, SW_CMD_NOT_ALLOWED)) {
      return "command not allowed";
    }
    return "----- UNKNOWN RETURN CODE!!! ------";
  }

  /**
   * Parses a Date object out of the given 2 byte arrays. The date and time is
   * strangely encoded in BCD format, which means you have to read it as
   * hexadeceimal string: for example:<br>
   * date: 0x131231<br>
   * time: 0x192355<br>
   * --> which represents 31. December 2013, 19:23:55
   * 
   * @param date
   * @param time
   * @return
   * @throws ParseException
   */
  public static Date getTimeStampFromBcdBytes(byte[] date, byte[] time)
      throws ParseException {
    if (date == null || date.length != 3) {
      throw new IllegalArgumentException(
          "getTimeStampFromBytes: date must be exactly 3 bytes long");
    }
    if (time == null || time.length != 3) {
      throw new IllegalArgumentException(
          "getTimeStampFromBytes: time must be exactly 3 bytes long");
    }
    DateFormat df = new SimpleDateFormat("yy MM dd  HH mm ss", Locale.US);
    return df.parse(prettyPrintString(bytesToHex(date), 2) + "  "
        + prettyPrintString(bytesToHex(time), 2));
  }

  /**
   * Parses a Date object out of the given byte array. The date is encoded in
   * BCD format, which means you have to read it as hexadeceimal string: for
   * example:<br>
   * date: 0x131231<br>
   * --> which represents 31. December 2013
   * 
   * @param date
   * @return date or null if all 3 bytes are 0
   * @throws ParseException
   */
  public static Date getDateFromBcdBytes(byte[] date) throws ParseException {
    if (date == null || date.length != 3) {
      throw new IllegalArgumentException(
          "getDateFromBcdBytes: date must be exactly 3 bytes long");
    }
    String s = prettyPrintString(bytesToHex(date), 2);
    if ("00 00 00".equals(s)) {
      return null;
    }
    DateFormat df = new SimpleDateFormat("yy MM dd", Locale.US);
    return df.parse(s);
  }

  /**
   * Parse timestamp from quick log entry
   * 
   * @param date
   * @param time
   * @return date or null if days are 0000000
   * @throws ParseException
   */
  public static Date getTimeStampFromQuickLog(int days, byte[] time) {
    if (days == 0) {
      return null;
    }
    if (time == null || time.length != 3) {
      throw new IllegalArgumentException(
          "getTimeStampFromQuickLog: time must be exactly 3 bytes long");
    }
    Calendar logDate = getDayFromQuickLogEntry(days);
    logDate.set(Calendar.HOUR_OF_DAY, Integer.parseInt(byte2Hex(time[0])));
    logDate.set(Calendar.MINUTE, Integer.parseInt(byte2Hex(time[1])));
    logDate.set(Calendar.SECOND, Integer.parseInt(byte2Hex(time[2])));
    return logDate.getTime();
  }

  /**
   * @param days
   *            number of days after September 02, 1975 (what happened on this
   *            day?)
   * @return
   */
  public static Calendar getDayFromQuickLogEntry(int days) {
    Calendar logDay = GregorianCalendar.getInstance();
    logDay.set(Calendar.DAY_OF_MONTH, 2);
    logDay.set(Calendar.MONTH, Calendar.SEPTEMBER);
    logDay.set(Calendar.YEAR, 1975);
    logDay.add(Calendar.DAY_OF_YEAR, days);
    return logDay;
  }

  /**
   * Read amount value from byte array
   * 
   * @param amount
   * @return
   */
  public static long getAmountFromBytes(byte[] amount) {
    if (amount == null || amount.length < 4) {
      throw new IllegalArgumentException(
          "getAmountFromBytes: needs at least 4 bytes");
    }
    return readLongFromBytes(amount, 0, 4);
  }

  /**
   * Read amount value from byte array whch holds the value in BCD format
   * (which means you have to read it as hex string to get the decimal
   * representation)
   * 
   * @param amount
   *            (example: 0x2345)
   * @return long value (example: 2345)
   */
  public static long getAmountFromBcdBytes(byte[] amount) {
    if (amount == null || amount.length != 6) {
      throw new IllegalArgumentException(
          "getAmountFromBcdBytes: needs 6 bytes");
    }
    return Long.parseLong(bytesToHex(amount));
  }

  /**
   * 
   * source: https://code.google.com/p/javaemvreader/
   * 
   * @param stream
   * @return
   * @throws NfcException
   */
  public static BERTLV getNextTLV(ByteArrayInputStream stream)
      throws TlvParsingException {
    if (stream.available() < 2) {
      throw new TlvParsingException(
          "Error parsing data. Available bytes < 2 . Length="
              + stream.available());
    }

    // ISO/IEC 7816 uses neither '00' nor 'FF' as tag value.
    // Before, between, or after TLV-coded data objects,
    // '00' or 'FF' bytes without any meaning may occur
    // (for example, due to erased or modified TLV-coded data objects).

    stream.mark(0);
    int peekInt = stream.read();
    byte peekByte = (byte) peekInt;
    // peekInt == 0xffffffff indicates EOS
    while (peekInt != -1
        && (peekByte == (byte) 0xFF || peekByte == (byte) 0x00)) {
      stream.mark(0); // Current position
      peekInt = stream.read();
      peekByte = (byte) peekInt;
    }
    stream.reset(); // Reset back to the last known position without 0x00 or
            // 0xFF

    if (stream.available() < 2) {
      throw new TlvParsingException(
          "Error parsing data. Available bytes < 2 . Length="
              + stream.available());
    }

    byte[] tagIdBytes = readTagIdBytes(stream);

    // We need to get the raw length bytes.
    // Use quick and dirty workaround
    stream.mark(0);
    int posBefore = stream.available();
    // Now parse the lengthbyte(s)
    // This method will read all length bytes. We can then find out how many
    // bytes was read.
    int length = readTagLength(stream);
    // Now find the raw length bytes
    int posAfter = stream.available();
    stream.reset();
    byte[] lengthBytes = new byte[posBefore - posAfter];
    stream.read(lengthBytes, 0, lengthBytes.length);

    int rawLength = byteArrayToInt(lengthBytes);

    byte[] valueBytes = null;

    EmvTag tag = EMVTags.getNotNull(tagIdBytes);

    // Find VALUE bytes
    if (rawLength == 128) { // 1000 0000
      // indefinite form
      stream.mark(0);
      int prevOctet = 1;
      int curOctet = 0;
      int len = 0;
      while (true) {
        len++;
        curOctet = stream.read();
        if (curOctet < 0) {
          throw new TlvParsingException(
              "Error parsing data. TLV "
                  + "length byte indicated indefinite length, but EOS "
                  + "was reached before 0x0000 was found"
                  + stream.available());
        }
        if (prevOctet == 0 && curOctet == 0) {
          break;
        }
        prevOctet = curOctet;
      }
      len -= 2;
      valueBytes = new byte[len];
      stream.reset();
      stream.read(valueBytes, 0, len);
      length = len;
    } else {
      // definite form
      valueBytes = new byte[length];
      stream.read(valueBytes, 0, length);
    }

    // Remove any trailing 0x00 and 0xFF
    stream.mark(0);
    peekInt = stream.read();
    peekByte = (byte) peekInt;
    while (peekInt != -1
        && (peekByte == (byte) 0xFF || peekByte == (byte) 0x00)) {
      stream.mark(0);
      peekInt = stream.read();
      peekByte = (byte) peekInt;
    }
    stream.reset(); // Reset back to the last known position without 0x00 or
            // 0xFF

    BERTLV tlv = new BERTLV(tag, length, lengthBytes, valueBytes);
    return tlv;
  }

  /**
   * Tries to parse a byte array as EMV BER-TLV encoded data and returns a
   * pretty formatted string (useful for logging and debugging output)<br>
   * 
   * source: https://code.google.com/p/javaemvreader/
   * 
   * @param data
   * @param indentLength
   * @return
   * @throws NfcException
   */
  public static String prettyPrintBerTlvAPDUResponse(byte[] data,
      int indentLength) throws TlvParsingException {
    StringBuilder buf = new StringBuilder();

    ByteArrayInputStream stream = new ByteArrayInputStream(data);

    while (stream.available() > 0) {
      buf.append("\n");

      buf.append(getSpaces(indentLength));

      BERTLV tlv = getNextTLV(stream);

      // Log.debug(tlv.toString());

      byte[] tagBytes = tlv.getTagBytes();
      byte[] lengthBytes = tlv.getRawEncodedLengthBytes();
      byte[] valueBytes = tlv.getValueBytes();

      EmvTag tag = tlv.getTag();

      // buf.append(" TAG: ");
      buf.append(prettyPrintString(bytesToHex(tagBytes), 2));
      buf.append("  -  ");
      buf.append(prettyPrintString(bytesToHex(lengthBytes), 2));
      buf.append(" bytes: ");
      buf.append(tag.getName());

      // int extraIndent = (lengthBytes.length * 3) + (tagBytes.length *
      // 3);
      int extraIndent = (lengthBytes.length * 2) + (tagBytes.length * 2);
      // int extraIndent = 2;

      if (tag.isConstructed()) {
        // indentLength += extraIndent;
        // Recursion
        buf.append(prettyPrintBerTlvAPDUResponse(valueBytes,
            indentLength + extraIndent));
      } else {
        buf.append("\n");
        if (tag.getTagValueType() == TagValueType.DOL) {
          buf.append(getFormattedTagAndLength(valueBytes,
              indentLength + extraIndent));
        } else {
          buf.append(getSpaces(indentLength + extraIndent));

          buf.append(prettyPrintHex(bytesToHex(valueBytes),
              indentLength + extraIndent));

          buf.append(" (");
          buf.append(getTagValueInfo(tag, valueBytes));
          buf.append(")");
        }
      }
    }
    return buf.toString();
  }

  /**
   * Tries to parse a byte array as EMV BER-TLV encoded data and returns a
   * list of tags
   * 
   * source: https://code.google.com/p/javaemvreader/
   * 
   * @param data
   * @param indentLength
   * @return
   * @throws NfcException
   */
  public static List<TagAndValue> getTagsFromBerTlvAPDUResponse(byte[] data)
      throws TlvParsingException {
    List<TagAndValue> tagList = new ArrayList<TagAndValue>();
    ByteArrayInputStream stream = new ByteArrayInputStream(data);
    while (stream.available() > 0) {
      BERTLV tlv = getNextTLV(stream);
      EmvTag tag = tlv.getTag();
      byte[] valueBytes = tlv.getValueBytes();

      if (tag.isConstructed()) {
        // Recursion:
        tagList.addAll(getTagsFromBerTlvAPDUResponse(tlv
            .getValueBytes()));
      } else {
        tagList.add(new TagAndValue(tag, valueBytes));
      }
    }
    return tagList;
  }

  /**
   * Filters interesting tags to be displayed in the result view
   * 
   * @param tagList
   * @return
   */
  public static List<InfoKeyValuePair> filterTagsForResult(Context ctx,
      List<TagAndValue> tagList, boolean cutOffLastAccountnumberDigit) {
    List<InfoKeyValuePair> resultList = new ArrayList<InfoKeyValuePair>();
    String tagBytesHexString;

    for (TagAndValue tagAndValue : tagList) {
      tagBytesHexString = bytesToHex(tagAndValue.getTag().getTagBytes());

      // Expiration date
      if ("5F24".equalsIgnoreCase(tagBytesHexString)) {
        try {
          InfoKeyValuePair expirationDate = new InfoKeyValuePair(ctx
              .getResources().getString(
                  R.string.lbl_expiration_date),
              formatDateOnly(getDateFromBcdBytes(tagAndValue
                  .getValue())));
          resultList.add(expirationDate);
        } catch (ParseException e) {
          // dont add in case we cannot parse
          Log.w(TAG, "cannot parse expiration date!", e);
        }
      }
      // Effective date
      else if ("5F25".equalsIgnoreCase(tagBytesHexString)) {
        try {
          InfoKeyValuePair expirationDate = new InfoKeyValuePair(ctx
              .getResources().getString(
                  R.string.lbl_effective_date),
              formatDateOnly(getDateFromBcdBytes(tagAndValue
                  .getValue())));
          resultList.add(expirationDate);
        } catch (ParseException e) {
          // dont add in case we cannot parse
          Log.w(TAG, "cannot parse effective date!", e);
        }
      }
      // Account Number
      else if ("5A".equalsIgnoreCase(tagBytesHexString)) {
        if (tagAndValue.getValue() != null
            && tagAndValue.getValue().length > 1) {
          String primaryAccountNumber = bytesToHex(tagAndValue
              .getValue());
          if (cutOffLastAccountnumberDigit) {
            // last character is always F: cut it off:
            primaryAccountNumber = primaryAccountNumber.substring(
                0, primaryAccountNumber.length() - 1);
          }
          resultList.add(new InfoKeyValuePair(ctx.getResources()
              .getString(R.string.lbl_primary_account_number),
              prettyPrintString(primaryAccountNumber, 4)));
        }
      }

      // Current ATC (application transaction counter) value
      else if ("9F36".equalsIgnoreCase(tagBytesHexString)) {
        if (tagAndValue.getValue() != null
            && tagAndValue.getValue().length > 1) {
          int currentTransactionCounter = byteArrayToInt(tagAndValue
              .getValue());
          resultList
              .add(new InfoKeyValuePair(
                  ctx.getResources()
                      .getString(
                          R.string.lbl_application_transaction_counter),
                  Integer.toString(currentTransactionCounter)));
        }
      }

      //
      // display mastercard specific(??) stuff
      //

      // Card risk management currency ?
      if ("C9".equalsIgnoreCase(tagBytesHexString)) {
        InfoKeyValuePair crmCurrency = new InfoKeyValuePair(ctx
            .getResources().getString(
                R.string.lbl_card_risk_management_currency),
            Iso4217CurrencyCodes.getCurrencyAsString(tagAndValue
                .getValue()));
        resultList.add(crmCurrency);
      }

      // country code for card risk management ?
      if ("C8".equalsIgnoreCase(tagBytesHexString)) {
        InfoKeyValuePair crmCountry = new InfoKeyValuePair(ctx
            .getResources().getString(
                R.string.lbl_card_risk_management_country),
            Iso3166CountryCodes.getCountryAsString(tagAndValue
                .getValue()));
        resultList.add(crmCountry);
      }

      // lower consecutive offline limit
      if ("9F14".equalsIgnoreCase(tagBytesHexString)) {
        InfoKeyValuePair kvPair = new InfoKeyValuePair(
            ctx.getResources().getString(
                R.string.lbl_lower_consecutive_offline_limit),
            Integer.toString(byteArrayToInt(tagAndValue.getValue())));
        resultList.add(kvPair);
      }

      // upper consecutive offline limit
      if ("9F23".equalsIgnoreCase(tagBytesHexString)) {
        InfoKeyValuePair kvPair = new InfoKeyValuePair(
            ctx.getResources().getString(
                R.string.lbl_upper_consecutive_offline_limit),
            Integer.toString(byteArrayToInt(tagAndValue.getValue())));
        resultList.add(kvPair);
      }

      // lower consecutive tx amount
      if ("CA".equalsIgnoreCase(tagBytesHexString)) {
        InfoKeyValuePair kvPair = new InfoKeyValuePair(
            ctx.getResources()
                .getString(
                    R.string.lbl_lower_consecutive_offline_tx_amount),
            formatBalance(getAmountFromBcdBytes(tagAndValue
                .getValue())));
        resultList.add(kvPair);
      }

      // upper consecutive tx amount
      if ("CB".equalsIgnoreCase(tagBytesHexString)) {
        InfoKeyValuePair kvPair = new InfoKeyValuePair(
            ctx.getResources()
                .getString(
                    R.string.lbl_upper_consecutive_offline_tx_amount),
            formatBalance(getAmountFromBcdBytes(tagAndValue
                .getValue())));
        resultList.add(kvPair);
      }

      // Log.d(TAG, "  name: " + tagAndValue.getTag().getName());
      // Log.d(TAG, "   tag: "
      // + bytesToHex(tagAndValue.getTag().getTagBytes()));
      // Log.d(TAG,
      // "   val: "
      // + getTagValueAsString(tagAndValue.getTag(),
      // tagAndValue.getValue()));

    }
    return resultList;
  }

  /**
   * Takes a date value as used in CPLC Date fields (represented by 2 bytes)
   * 
   * @param paramByte1
   * @param paramByte2
   * @throws IllegalArgumentException
   * @return
   */
  public static Date calculateCplcDate(byte[] dateBytes)
      throws IllegalArgumentException {
    if (dateBytes == null || dateBytes.length != 2) {
      throw new IllegalArgumentException(
          "Error! CLCP Date values consist always of exactly 2 bytes");
    }
    // current time
    Calendar now = Calendar.getInstance();

    int year = now.get(Calendar.YEAR);
    int startYearOfCurrentDecade = year - (year % 10);

    int days = 100 * (dateBytes[0] & 0xF) + 10 * (0xF & dateBytes[1] >>> 4)
        + (dateBytes[1] & 0xF);

    if (days > 366) {
      throw new IllegalArgumentException(
          "Invalid date (or are we parsing it wrong??)");
    }

    Calendar calculatedDate = Calendar.getInstance();
    calculatedDate.clear();
    calculatedDate.set(Calendar.YEAR, startYearOfCurrentDecade
        + (0xF & dateBytes[0] >>> 4));
    calculatedDate.set(Calendar.DAY_OF_YEAR, days);
    while (calculatedDate.after(now)) {
      calculatedDate.add(Calendar.YEAR, -10);
    }
    return calculatedDate.getTime();
  }

  /**
   * CPLC dates are counted in days starting from "1986-02-04"
   * 
   * @param cplcDateValue
   * @return
   */
  public static Calendar getDateFromCPLCDateValue(int cplcDateValue) {
    GregorianCalendar startDay = new GregorianCalendar(1977, 11, 29);
    startDay.add(Calendar.DAY_OF_YEAR, cplcDateValue);
    return startDay;
  }

  /**
   * checks if the given 3 byte long array looks like a valid BCD encoded date
   * value
   * 
   * @param time
   * @return
   */
  @SuppressWarnings("unused")
  private static boolean bytesLookLikeValidDate(byte[] date) {
    int digit;
    try {
      // year byte
      Integer.parseInt(byte2Hex(date[0]));
      // month byte
      digit = Integer.parseInt(byte2Hex(date[1]));
      if (digit < 1 || digit > 12)
        return false;
      // day byte
      digit = Integer.parseInt(byte2Hex(date[2]));
      if (digit < 1 || digit > 31)
        return false;
    } catch (NumberFormatException nfe) {
      return false;
    }
    return true;
  }

  /**
   * checks if the given 3 byte long array looks like a valid BCD encoded time
   * 
   * @param time
   * @return
   */
  @SuppressWarnings("unused")
  private static boolean bytesLookLikeValidTime(byte[] time) {
    int digit;
    try {
      // hour byte
      digit = Integer.parseInt(byte2Hex(time[0]));
      if (digit > 23)
        return false;
      // minutes byte
      digit = Integer.parseInt(byte2Hex(time[1]));
      if (digit > 59)
        return false;
      // seconds byte
      digit = Integer.parseInt(byte2Hex(time[2]));
      if (digit > 59)
        return false;
    } catch (NumberFormatException nfe) {
      return false;
    }
    return true;
  }

  /**
   * read tag id bytes from EMV tag bytestream
   * 
   * source: https://code.google.com/p/javaemvreader/
   * 
   * @param stream
   * @return
   */
  private static byte[] readTagIdBytes(ByteArrayInputStream stream) {
    ByteArrayOutputStream tagBAOS = new ByteArrayOutputStream();
    byte tagFirstOctet = (byte) stream.read();
    tagBAOS.write(tagFirstOctet);

    // Find TAG bytes
    byte MASK = (byte) 0x1F;
    if ((tagFirstOctet & MASK) == MASK) { // EMV book 3, Page 178 or Annex
                        // B1 (EMV4.3)
      // Tag field is longer than 1 byte
      do {
        int nextOctet = stream.read();
        if (nextOctet < 0) {
          break;
        }
        byte tlvIdNextOctet = (byte) nextOctet;

        tagBAOS.write(tlvIdNextOctet);

        if (!isBitSet(tlvIdNextOctet, 8)) {
          break;
        }
      } while (true);
    }
    return tagBAOS.toByteArray();
  }

  /**
   * read length value from EMV tag bytestream
   * 
   * source: https://code.google.com/p/javaemvreader/
   * 
   * @param stream
   * @return
   */
  private static int readTagLength(ByteArrayInputStream stream) {
    // Find LENGTH bytes
    int length;
    int tmpLength = stream.read();

    if (tmpLength <= 127) { // 0111 1111
      // short length form
      length = tmpLength;
    } else if (tmpLength == 128) { // 1000 0000
      // length identifies indefinite form, will be set later
      length = tmpLength;
    } else {
      // long length form
      int numberOfLengthOctets = tmpLength & 127; // turn off 8th bit
      tmpLength = 0;
      for (int i = 0; i < numberOfLengthOctets; i++) {
        int nextLengthOctet = stream.read();
        tmpLength <<= 8;
        tmpLength |= nextLengthOctet;
      }
      length = tmpLength;
    }
    return length;
  }

  /**
   * Returns a string representation of a list of Tag and Lengths (eg DOLs)<br>
   * 
   * source: https://code.google.com/p/javaemvreader/
   * 
   * @param data
   * @param indentLength
   * @return
   */
  private static String getFormattedTagAndLength(byte[] data, int indentLength) {
    StringBuilder buf = new StringBuilder();
    String indent = getSpaces(indentLength);
    ByteArrayInputStream stream = new ByteArrayInputStream(data);

    boolean firstLine = true;
    while (stream.available() > 0) {
      if (firstLine) {
        firstLine = false;
      } else {
        buf.append("\n");
      }
      buf.append(indent);

      EmvTag tag = EMVTags.getNotNull(readTagIdBytes(stream));
      int length = readTagLength(stream);

      buf.append(prettyPrintString(bytesToHex(tag.getTagBytes()), 2));
      buf.append(" (");
      buf.append(bytesToHex(intToByteArray(length)));
      buf.append(" bytes) -> ");
      buf.append(tag.getName());
    }
    return buf.toString();
  }

  /**
   * Tag value as string
   * 
   * source: https://code.google.com/p/javaemvreader/
   * 
   * @param tag
   * @param value
   * @return
   */
  private static String getTagValueInfo(EmvTag tag, byte[] value) {
    StringBuilder buf = new StringBuilder();
    switch (tag.getTagValueType()) {
    case TEXT:
      buf.append("=");
      buf.append(new String(value));
      break;
    case NUMERIC:
      buf.append("NUMERIC");
      break;
    case BINARY:
      buf.append("BINARY");
      break;

    case MIXED:
      buf.append("=");
      buf.append(getSafePrintChars(value));
      break;

    case DOL:
      buf.append("");
      break;
    default:
      buf.append("");
      break;
    }
    return buf.toString();
  }

  // This prints all non-control characters common to all parts of ISO/IEC
  // 8859
  // See EMV book 4 Annex B: Table 36: Common Character Set
  // source: https://code.google.com/p/javaemvreader/
  private static String getSafePrintChars(byte[] byteArray) {
    if (byteArray == null) {
      // return "" instead?
      throw new IllegalArgumentException(
          "Argument 'byteArray' cannot be null");
    }
    return getSafePrintChars(byteArray, 0, byteArray.length);
  }

  // source: https://code.google.com/p/javaemvreader/
  private static String getSafePrintChars(byte[] byteArray, int startPos,
      int length) {
    if (byteArray == null) {
      // return "" instead?
      throw new IllegalArgumentException(
          "Argument 'byteArray' cannot be null");
    }
    if (byteArray.length < startPos + length) {
      throw new IllegalArgumentException("startPos(" + startPos
          + ")+length(" + length + ") > byteArray.length("
          + byteArray.length + ")");
    }
    StringBuilder buf = new StringBuilder();
    for (int i = startPos; i < length; i++) {
      if (byteArray[i] >= (byte) 0x20 && byteArray[i] < (byte) 0x7F) {
        buf.append((char) byteArray[i]);
      } else {
        buf.append(".");
      }
    }
    return buf.toString();
  }

}




Java Source Code List

at.zweng.bankomatinfos.AppController.java
at.zweng.bankomatinfos.exceptions.NoSmartCardException.java
at.zweng.bankomatinfos.exceptions.TlvParsingException.java
at.zweng.bankomatinfos.iso7816emv.BERTLV.java
at.zweng.bankomatinfos.iso7816emv.ByteArrayWrapper.java
at.zweng.bankomatinfos.iso7816emv.CPLC.java
at.zweng.bankomatinfos.iso7816emv.EMVTags.java
at.zweng.bankomatinfos.iso7816emv.EmvTag.java
at.zweng.bankomatinfos.iso7816emv.EmvUtils.java
at.zweng.bankomatinfos.iso7816emv.GPTags.java
at.zweng.bankomatinfos.iso7816emv.Iso3166CountryCodes.java
at.zweng.bankomatinfos.iso7816emv.Iso4217CurrencyCodes.java
at.zweng.bankomatinfos.iso7816emv.NfcBankomatCardReader.java
at.zweng.bankomatinfos.iso7816emv.TagAndValue.java
at.zweng.bankomatinfos.iso7816emv.TagImpl.java
at.zweng.bankomatinfos.iso7816emv.TagType.java
at.zweng.bankomatinfos.iso7816emv.TagValueType.java
at.zweng.bankomatinfos.model.AbstractTransactionLogEntry.java
at.zweng.bankomatinfos.model.CardInfo.java
at.zweng.bankomatinfos.model.EmvTransactionLogEntry.java
at.zweng.bankomatinfos.model.InfoKeyValuePair.java
at.zweng.bankomatinfos.model.QuickTransactionLogEntry.java
at.zweng.bankomatinfos.ui.AboutDialogFragment.java
at.zweng.bankomatinfos.ui.ChangelogDialogFragment.java
at.zweng.bankomatinfos.ui.ListAdapterEmvTransactions.java
at.zweng.bankomatinfos.ui.ListAdapterInfos.java
at.zweng.bankomatinfos.ui.ListAdapterQuickTransactions.java
at.zweng.bankomatinfos.ui.MainActivity.java
at.zweng.bankomatinfos.ui.NfcDisabledActivity.java
at.zweng.bankomatinfos.ui.ResultActivity.java
at.zweng.bankomatinfos.ui.ResultEmvTxListFragment.java
at.zweng.bankomatinfos.ui.ResultInfosListFragment.java
at.zweng.bankomatinfos.ui.ResultLogFragment.java
at.zweng.bankomatinfos.ui.ResultQuickTxListFragment.java
at.zweng.bankomatinfos.ui.SettingsActivity.java
at.zweng.bankomatinfos.util.ChangeLog.java
at.zweng.bankomatinfos.util.CustomAlertDialog.java
at.zweng.bankomatinfos.util.Utils.java