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() {
}
}