com.github.devnied.emvnfccard.utils.TlvUtil.java Source code

Java tutorial

Introduction

Here is the source code for com.github.devnied.emvnfccard.utils.TlvUtil.java

Source

package com.github.devnied.emvnfccard.utils;

/*
 * Copyright 2010 sasc
 *
 * 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.
 */

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 com.github.devnied.emvnfccard.enums.SwEnum;
import com.github.devnied.emvnfccard.enums.TagValueTypeEnum;
import com.github.devnied.emvnfccard.exception.TlvException;
import com.github.devnied.emvnfccard.iso7816emv.EmvTags;
import com.github.devnied.emvnfccard.iso7816emv.ITag;
import com.github.devnied.emvnfccard.iso7816emv.TLV;
import com.github.devnied.emvnfccard.iso7816emv.TagAndLength;

import fr.devnied.bitlib.BytesUtils;

/**
 * List of utils methods to manipulate TLV
 * 
 * @author MILLAU Julien
 * 
 */
public final class TlvUtil {

    /**
     * Method used to find Tag with ID
     * 
     * @param tagIdBytes
     *            the tag to find
     * @return the tag found
     */
    private static ITag searchTagById(final byte[] tagIdBytes) {
        return EmvTags.getNotNull(tagIdBytes); // TODO take app (IIN or RID) into consideration
    }

    /**
     * Method used to search tag with id
     * 
     * @param stream
     *            Byte array in a stream
     * @return Tag found
     */
    private static ITag searchTagById(final ByteArrayInputStream stream) {
        return searchTagById(TlvUtil.readTagIdBytes(stream));
    }

    // This is just a list of Tag And Lengths (eg DOLs)
    public static String getFormattedTagAndLength(final byte[] data, final 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);

            ITag tag = searchTagById(stream);
            int length = TlvUtil.readTagLength(stream);

            buf.append(prettyPrintHex(tag.getTagBytes()));
            buf.append(" ");
            buf.append(String.format("%02x", length));
            buf.append(" -- ");
            buf.append(tag.getName());
        }
        return buf.toString();
    }

    public static byte[] readTagIdBytes(final 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 (!BytesUtils.matchBitByBitIndex(tlvIdNextOctet, 7)
                        || BytesUtils.matchBitByBitIndex(tlvIdNextOctet, 7) && (tlvIdNextOctet & 0x7f) == 0) {
                    break;
                }
            } while (true);
        }
        return tagBAOS.toByteArray();
    }

    public static int readTagLength(final ByteArrayInputStream stream) {
        // Find LENGTH bytes
        int length;
        int tmpLength = stream.read();

        if (tmpLength < 0) {
            throw new TlvException("Negative length: " + tmpLength);
        }

        if (tmpLength <= 127) { // 0111 1111
            // short length form
            length = tmpLength;
        } else if (tmpLength == 128) { // 1000 0000
            // length identifies indefinite form, will be set later
            // indefinite form is not specified in ISO7816-4, but we include it here for completeness
            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();
                if (nextLengthOctet < 0) {
                    throw new TlvException("EOS when reading length bytes");
                }
                tmpLength <<= 8;
                tmpLength |= nextLengthOctet;
            }
            length = tmpLength;
        }
        return length;
    }

    public static TLV getNextTLV(final ByteArrayInputStream stream) {
        if (stream.available() < 2) {
            throw new TlvException("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 TlvException("Error parsing data. Available bytes < 2 . Length=" + stream.available());
        }

        byte[] tagIdBytes = TlvUtil.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 = TlvUtil.readTagLength(stream); // Decoded
        // Now find the raw (encoded) length bytes
        int posAfter = stream.available();
        stream.reset();
        byte[] lengthBytes = new byte[posBefore - posAfter];

        if (lengthBytes.length < 1 || lengthBytes.length > 4) {
            throw new TlvException("Number of length bytes must be from 1 to 4. Found " + lengthBytes.length);
        }

        stream.read(lengthBytes, 0, lengthBytes.length);

        int rawLength = BytesUtils.byteArrayToInt(lengthBytes);

        byte[] valueBytes;

        ITag tag = searchTagById(tagIdBytes);

        // Find VALUE bytes
        if (rawLength == 128) { // 1000 0000
            // indefinite form
            stream.mark(0);
            int prevOctet = 1;
            int curOctet;
            int len = 0;
            while (true) {
                len++;
                curOctet = stream.read();
                if (curOctet < 0) {
                    throw new TlvException(
                            "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 {
            if (stream.available() < length) {
                throw new TlvException("Length byte(s) indicated " + length + " value bytes, but only "
                        + stream.available() + " " + (stream.available() > 1 ? "are" : "is") + " available");
            }
            // 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

        return new TLV(tag, length, lengthBytes, valueBytes);
    }

    /**
     * Method used get Tag value as String
     * 
     * @param tag
     *            tag type
     * @param value
     *            tag value
     * @return
     */
    private static String getTagValueAsString(final ITag tag, final 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:
            break;
        }

        return buf.toString();
    }

    /**
     * Method used to parser Tag and length
     * 
     * @param data
     *            data to parse
     * @return list of tag and length
     */
    public static List<TagAndLength> parseTagAndLength(final byte[] data) {
        List<TagAndLength> tagAndLengthList = new ArrayList<TagAndLength>();
        if (data != null) {
            ByteArrayInputStream stream = new ByteArrayInputStream(data);

            while (stream.available() > 0) {
                if (stream.available() < 2) {
                    throw new TlvException("Data length < 2 : " + stream.available());
                }

                ITag tag = searchTagById(TlvUtil.readTagIdBytes(stream));
                int tagValueLength = TlvUtil.readTagLength(stream);

                tagAndLengthList.add(new TagAndLength(tag, tagValueLength));
            }
        }
        return tagAndLengthList;
    }

    public static String prettyPrintAPDUResponse(final byte[] data) {
        return prettyPrintAPDUResponse(data, 0);
    }

    public static String prettyPrintAPDUResponse(final byte[] data, final int startPos, final int length) {
        byte[] tmp = new byte[length - startPos];
        System.arraycopy(data, startPos, tmp, 0, length);
        return prettyPrintAPDUResponse(tmp, 0);
    }

    /**
     * Method used to get the list of TLV inside the parameter tag specified in parameter
     * 
     * @param pData
     *            data to parse
     * @param pTag
     *            tag to find
     * @param pAdd
     * @return the list of TLV tag inside
     */
    public static List<TLV> getlistTLV(final byte[] pData, final ITag pTag, final boolean pAdd) {

        List<TLV> list = new ArrayList<TLV>();

        ByteArrayInputStream stream = new ByteArrayInputStream(pData);

        while (stream.available() > 0) {

            TLV tlv = TlvUtil.getNextTLV(stream);
            if (pAdd) {
                list.add(tlv);
            } else if (tlv.getTag().isConstructed()) {
                list.addAll(TlvUtil.getlistTLV(tlv.getValueBytes(), pTag, tlv.getTag() == pTag));
            }
        }

        return list;
    }

    /**
     * Method used to get the list of TLV corresponding to tags specified in parameters
     * 
     * @param pData
     *            data to parse
     * @param pTag
     *            tags to find
     * @param pAdd
     * @return the list of TLV
     */
    public static List<TLV> getlistTLV(final byte[] pData, final ITag... pTag) {

        List<TLV> list = new ArrayList<TLV>();

        ByteArrayInputStream stream = new ByteArrayInputStream(pData);

        while (stream.available() > 0) {

            TLV tlv = TlvUtil.getNextTLV(stream);
            if (ArrayUtils.contains(pTag, tlv.getTag())) {
                list.add(tlv);
            } else if (tlv.getTag().isConstructed()) {
                list.addAll(TlvUtil.getlistTLV(tlv.getValueBytes(), pTag));
            }
        }

        return list;
    }

    /**
     * Method used to get Tag value
     * 
     * @param pData
     *            data
     * @param pTag
     *            tag to find
     * @return tag value or null
     */
    public static byte[] getValue(final byte[] pData, final ITag... pTag) {

        byte[] ret = null;

        if (pData != null) {
            ByteArrayInputStream stream = new ByteArrayInputStream(pData);

            while (stream.available() > 0) {

                TLV tlv = TlvUtil.getNextTLV(stream);
                if (ArrayUtils.contains(pTag, tlv.getTag())) {
                    return tlv.getValueBytes();
                } else if (tlv.getTag().isConstructed()) {
                    ret = TlvUtil.getValue(tlv.getValueBytes(), pTag);
                    if (ret != null) {
                        break;
                    }
                }
            }
        }

        return ret;
    }

    public static String prettyPrintAPDUResponse(final byte[] data, final int indentLength) {
        StringBuilder buf = new StringBuilder();

        ByteArrayInputStream stream = new ByteArrayInputStream(data);

        while (stream.available() > 0) {
            buf.append("\n");
            if (stream.available() == 2) {
                stream.mark(0);
                byte[] value = new byte[2];
                try {
                    stream.read(value);
                } catch (IOException e) {
                }
                SwEnum sw = SwEnum.getSW(value);
                if (sw != null) {
                    buf.append(getSpaces(0));
                    buf.append(BytesUtils.bytesToString(value)).append(" -- ");
                    buf.append(sw.getDetail());
                    continue;
                }
                stream.reset();
            }

            buf.append(getSpaces(indentLength));

            TLV tlv = TlvUtil.getNextTLV(stream);

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

            ITag tag = tlv.getTag();

            buf.append(prettyPrintHex(tagBytes));
            buf.append(" ");
            buf.append(prettyPrintHex(lengthBytes));
            buf.append(" -- ");
            buf.append(tag.getName());

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

            if (tag.isConstructed()) {
                // indentLength += extraIndent; //TODO check this
                // Recursion
                buf.append(prettyPrintAPDUResponse(valueBytes, indentLength + extraIndent));
            } else {
                buf.append("\n");
                if (tag.getTagValueType() == TagValueTypeEnum.DOL) {
                    buf.append(TlvUtil.getFormattedTagAndLength(valueBytes, indentLength + extraIndent));
                } else {
                    buf.append(getSpaces(indentLength + extraIndent));
                    buf.append(prettyPrintHex(BytesUtils.bytesToStringNoSpace(valueBytes),
                            indentLength + extraIndent));
                    buf.append(" (");
                    buf.append(TlvUtil.getTagValueAsString(tag, valueBytes));
                    buf.append(")");
                }
            }
        }
        return buf.toString();
    }

    public static String getSpaces(final int length) {
        return StringUtils.leftPad(StringUtils.EMPTY, length);
    }

    public static String prettyPrintHex(final String in, final int indent) {
        return prettyPrintHex(in, indent, true);
    }

    public static String prettyPrintHex(final byte[] data, final int indent) {
        return prettyPrintHex(BytesUtils.bytesToStringNoSpace(data), indent, true);
    }

    public static String prettyPrintHex(final String in) {
        return prettyPrintHex(in, 0, true);
    }

    public static String prettyPrintHex(final byte[] data) {
        return prettyPrintHex(BytesUtils.bytesToStringNoSpace(data), 0, true);
    }

    public static String prettyPrintHex(final String in, final int indent, final boolean wrapLines) {
        StringBuilder buf = new StringBuilder();

        for (int i = 0; i < in.length(); i++) {
            char c = in.charAt(i);
            buf.append(c);

            int nextPos = i + 1;
            if (wrapLines && nextPos % 32 == 0 && nextPos != in.length()) {
                buf.append("\n").append(getSpaces(indent));
            } else if (nextPos % 2 == 0 && nextPos != in.length()) {
                buf.append(" ");
            }
        }
        return buf.toString();
    }

    public static String getSafePrintChars(final byte[] byteArray) {
        if (byteArray == null) {
            return "";
        }
        return getSafePrintChars(byteArray, 0, byteArray.length);
    }

    public static String getSafePrintChars(final byte[] byteArray, final int startPos, final int length) {
        if (byteArray == null) {
            return "";
        }
        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 < startPos + length; i++) {
            if (byteArray[i] >= (byte) 0x20 && byteArray[i] < (byte) 0x7F) {
                buf.append((char) byteArray[i]);
            } else {
                buf.append(".");
            }
        }
        return buf.toString();
    }

    /**
     * Method used to get length of all Tags
     * 
     * @param pList
     *            tag length list
     * @return the sum of tag length
     */
    public static int getLength(final List<TagAndLength> pList) {
        int ret = 0;
        if (pList != null) {
            for (TagAndLength tl : pList) {
                ret += tl.getLength();
            }
        }
        return ret;
    }

    /**
     * Private constructor
     */
    private TlvUtil() {
    }

}