/*
* SCUBA smart card framework.
*
* Copyright (C) 2009 The SCUBA team.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* $Id: BERTLVObject.java 1865 2011-09-27 13:22:17Z anatom $
*/
/*
* This file has been re-licensed under LGPL with permission from
* Martijn Oostdijk, 2009-08-15.
*/
package net.sourceforge.scuba.tlv;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import net.sourceforge.scuba.util.Hex;
/**
* Generic data structure for storing Tag Length Value (TLV) objects encoded
* according to the Basic Encoding Rules (BER). Written by Martijn Oostdijk (MO)
* and Cees-Bart Breunesse (CB) of the Security of Systems group (SoS) of the
* Institute of Computing and Information Sciences (ICIS) at Radboud University
* (RU). Based on ISO 7816-4 Annex D (which apparently is based on ISO 8825
* and/or X.208, X.209, X.680, X.690). See ASN.1.
*
* @author Martijn Oostdijk (martijno@cs.ru.nl)
* @author Cees-Bart Breunesse (ceesb@cs.ru.nl)
* @version $Revision: 227 $
*/
public class BERTLVObject
{
private static final SimpleDateFormat SDF = new SimpleDateFormat("yyMMddhhmmss'Z'");
/** Universal tag class. */
public static final int UNIVERSAL_CLASS = 0;
/** Application tag class. */
public static final int APPLICATION_CLASS = 1;
/** Context specific tag class. */
public static final int CONTEXT_SPECIFIC_CLASS = 2;
/** Private tag class. */
public static final int PRIVATE_CLASS = 3;
/** Universal tag type. */
public static final int BOOLEAN_TYPE_TAG = 0x01, INTEGER_TYPE_TAG = 0x02,
BIT_STRING_TYPE_TAG = 0x03, OCTET_STRING_TYPE_TAG = 0x04,
NULL_TYPE_TAG = 0x05, OBJECT_IDENTIFIER_TYPE_TAG = 0x06,
OBJECT_DESCRIPTOR_TYPE_TAG = 0x07, EXTERNAL_TYPE_TAG = 0x08,
REAL_TYPE_TAG = 0x09, ENUMERATED_TYPE_TAG = 0x0A,
EMBEDDED_PDV_TYPE_TAG = 0x0B, UTF8_STRING_TYPE_TAG = 0x0C,
SEQUENCE_TYPE_TAG = 0x10, SET_TYPE_TAG = 0x11,
NUMERIC_STRING_TYPE_TAG = 0x12, PRINTABLE_STRING_TYPE_TAG = 0x13,
T61_STRING_TYPE_TAG = 0x14, IA5_STRING_TYPE_TAG = 0x16,
UTC_TIME_TYPE_TAG = 0x17, GENERALIZED_TIME_TYPE_TAG = 0x18,
GRAPHIC_STRING_TYPE_TAG = 0x19, VISIBLE_STRING_TYPE_TAG = 0x1A,
GENERAL_STRING_TYPE_TAG = 0x1B, UNIVERSAL_STRING_TYPE_TAG = 0x1C,
BMP_STRING_TYPE_TAG = 0x1E;
/** Tag. */
private int tag;
/** Length. */
private int length;
/** Value, is usually just a byte[]. */
private Object value;
/**
* Constructs a new TLV object with tag tag
containing data
* value
.
*
* @param tag tag of TLV object
* @param value data of TLV object
* @throws IOException if something goes wrong.
*/
public BERTLVObject(int tag, Object value) {
this(tag, value, true);
}
/**
* Constructs a new TLV object with tag tag
containing data
* value
.
*
* @param tag tag of TLV object
* @param value data of TLV object
* @param interpretValue whether the embedded byte[] values should be
* interpreted/parsed. Some ASN1 streams don't like that :(
* @throws IOException if something goes wrong.
*/
public BERTLVObject(int tag, Object value, boolean interpretValue) {
try {
this.tag = tag;
this.value = value;
if (value instanceof byte[]) {
this.length = ((byte[])value).length;
} else if (value instanceof BERTLVObject) {
this.value = new BERTLVObject[1];
((BERTLVObject[])this.value)[0] = (BERTLVObject)value;
} else if (value instanceof BERTLVObject[]) {
this.value = value;
} else if (value instanceof Byte) {
this.length = 1;
this.value = new byte[1];
((byte[])this.value)[0] = ((Byte)value).byteValue();
} else if (value instanceof Integer) {
this.value = new BERTLVObject[1];
((BERTLVObject[])this.value)[0] = new BERTLVObject(INTEGER_TYPE_TAG, value);
} else {
throw new IllegalArgumentException("Cannot encode value of type: " + value.getClass());
}
if (value instanceof byte[] && interpretValue) {
this.value = interpretValue(tag, (byte[])value);
}
// reconstructLength();
} catch (Exception e) {
e.printStackTrace();
}
}
public static BERTLVObject getInstance(
InputStream in) throws IOException {
BERTLVInputStream tlvIn = (in instanceof BERTLVInputStream) ? (BERTLVInputStream)in : new BERTLVInputStream(in);
int tag = tlvIn.readTag();
tlvIn.readLength();
byte[] valueBytes = tlvIn.readValue();
BERTLVObject result = new BERTLVObject(tag, valueBytes);
return result;
}
private static Object interpretValue(int tag, byte[] valueBytes) {
if (isPrimitive(tag)) {
return interpretPrimitiveValue(tag, valueBytes);
} else {
/*
* Not primitive, the value itself consists of 0 or more BER-TLV
* objects.
*/
try {
return interpretCompoundValue(tag, valueBytes);
} catch (IOException ioe) {
return new BERTLVObject[0];
}
}
}
/*
* Primitive, the value consists of 0 or more Simple-TLV objects, or
* just (application-dependent) bytes. If tag is not known (or
* universal) we assume the value is just bytes.
*/
private static Object interpretPrimitiveValue(int tag, byte[] valueBytes) {
if (getTagClass(tag) == UNIVERSAL_CLASS)
switch (tag) {
case INTEGER_TYPE_TAG:
case BIT_STRING_TYPE_TAG:
case OCTET_STRING_TYPE_TAG:
case NULL_TYPE_TAG:
case OBJECT_IDENTIFIER_TYPE_TAG:
return valueBytes;
case UTF8_STRING_TYPE_TAG:
case PRINTABLE_STRING_TYPE_TAG:
case T61_STRING_TYPE_TAG:
case IA5_STRING_TYPE_TAG:
case VISIBLE_STRING_TYPE_TAG:
case GENERAL_STRING_TYPE_TAG:
case UNIVERSAL_STRING_TYPE_TAG:
case BMP_STRING_TYPE_TAG:
return new String(valueBytes);
case UTC_TIME_TYPE_TAG:
try { return SDF.parse(new String(valueBytes)); } catch (ParseException pe) { }
}
return valueBytes;
}
private static BERTLVObject[] interpretCompoundValue(int tag, byte[] valueBytes)
throws IOException {
Collection subObjects = new ArrayList();
BERTLVInputStream in = new BERTLVInputStream(new ByteArrayInputStream(valueBytes));
int length = valueBytes.length;
try {
while (length > 0) {
BERTLVObject subObject = BERTLVObject.getInstance(in);
length -= subObject.getLength();
subObjects.add(subObject);
}
} catch (EOFException eofe) { }
BERTLVObject[] result = new BERTLVObject[subObjects.size()];
subObjects.toArray(result);
return result;
}
private static int getTagClass(int tag) {
int i = 3;
for (; i >= 0; i--) {
int mask = (0xFF << (8 * i));
if ((tag & mask) != 0x00) { break; }
}
int msByte = (((tag & (0xFF << (8 * i))) >> (8 * i)) & 0xFF);
switch (msByte & 0xC0) {
case 0x00: return UNIVERSAL_CLASS;
case 0x40: return APPLICATION_CLASS;
case 0x80: return CONTEXT_SPECIFIC_CLASS;
case 0xC0:
default: return PRIVATE_CLASS;
}
}
/****************************************************************************
* Adds
* object as subobject of this
TLV object when
* this
is not a primitive object.
*
* @param object to add as a subobject.
*/
public void addSubObject(BERTLVObject object) {
Collection subObjects = new ArrayList();
if (value == null) {
value = new BERTLVObject[1];
} else if (value instanceof BERTLVObject[]){
subObjects.addAll(Arrays.asList((BERTLVObject[])value));
} else if (value instanceof BERTLVObject){
/* NOTE: Should never happen if indeed !isPrimitive... */
subObjects.add((BERTLVObject)value);
value = new BERTLVObject[2];
} else {
throw new IllegalStateException("Error: Unexpected value in BERTLVObject");
}
subObjects.add(object);
value = subObjects.toArray((BERTLVObject[])value);
reconstructLength();
}
public int getTag() {
return tag;
}
/**
* Reconstructs the length of the encoded value.
*/
public void reconstructLength() {
/* NOTE: needed after sub-objects have been added. */
length = getValueAsBytes(tag, value).length;
}
public int getLength() {
return length;
}
/**
* The encoded value.
*
* @return the encoded value
*/
public Object getValue() {
return value;
}
/**
* This object, including tag and length, as byte array.
*
* @return this object, including tag and length, as byte array
*/
public byte[] getEncoded() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
out.write(getTagAsBytes(tag));
out.write(getLengthAsBytes(getLength()));
out.write(getValueAsBytes(tag, value));
} catch (IOException ioe) {
ioe.printStackTrace();
}
return out.toByteArray();
}
/**
* Gets the first sub-object (including this object) whose tag equals
* tag
.
*
* @param tag the tag to search for
* @return the first
*/
public BERTLVObject getSubObject(int tag) {
if (this.tag == tag) {
return this;
} else if (value instanceof BERTLVObject[]) {
BERTLVObject[] children = (BERTLVObject[])value;
for (int i = 0; i < children.length; i++) {
BERTLVObject child = children[i];
BERTLVObject candidate = child.getSubObject(tag);
if (candidate != null) { return candidate; }
}
}
return null;
}
/**
* Gets the first sub-object (including this object) following the tags in
* tagPath.
*
* @param tagPath the path to follow
* @param offset in the tagPath
* @param length of the tagPath
* @return the first
*/
public BERTLVObject getSubObject(int[] tagPath, int offset, int length) {
if (length == 0) {
return this;
} else {
BERTLVObject child = getSubObject(tagPath[offset]);
if (child != null) { return child.getSubObject(tagPath, offset + 1,
length - 1); }
}
return null;
}
/****************************************************************************
* Returns the indexed child (starting from 0) or null otherwise.
*
* @param index
* @return the object pointed to by index.
*/
public BERTLVObject getChildByIndex(int index) {
if (value instanceof BERTLVObject[]) {
BERTLVObject[] children = (BERTLVObject[])value;
return children[index];
}
return null;
}
/**
* A textual (nested tree-like) representation of this object. Always ends in
* newline character, no need to add it yourself.
*
* @return a textual representation of this object.
* @see java.lang.Object#toString()
*/
public String toString() {
return toString(0);
}
private String toString(int indent) {
byte[] prefixBytes = new byte[indent];
Arrays.fill(prefixBytes, (byte)' ');
String prefix = new String(prefixBytes);
StringBuffer result = new StringBuffer();
result.append(prefix);
result.append(tagToString());
result.append(" ");
result.append(Integer.toString(getLength()));
result.append(" ");
if (value instanceof byte[]) {
byte[] valueData = (byte[])value;
result.append("'0x");
if (indent + 2 * valueData.length <= 60) {
result.append(Hex.bytesToHexString(valueData));
} else {
result
.append(Hex.bytesToHexString(valueData, 0, (50 - indent) / 2));
result.append("...");
}
result.append("'\n");
} else if (value instanceof BERTLVObject[]) {
result.append("{\n");
BERTLVObject[] subObjects = (BERTLVObject[])value;
for (int i = 0; i < subObjects.length; i++) {
result.append(subObjects[i].toString(indent + 3));
}
result.append(prefix);
result.append("}\n");
} else {
result.append("\"");
result.append(value != null ? value.toString() : "null");
result.append("\"\n");
}
return result.toString();
}
private String tagToString() {
if (getTagClass(tag) == UNIVERSAL_CLASS) {
if (isPrimitive(tag)) {
switch (tag & 0x1F) {
case BOOLEAN_TYPE_TAG:
return "BOOLEAN";
case INTEGER_TYPE_TAG:
return "INTEGER";
case BIT_STRING_TYPE_TAG:
return "BIT_STRING";
case OCTET_STRING_TYPE_TAG:
return "OCTET_STRING";
case NULL_TYPE_TAG:
return "NULL";
case OBJECT_IDENTIFIER_TYPE_TAG:
return "OBJECT_IDENTIFIER";
case REAL_TYPE_TAG:
return "REAL";
case UTF8_STRING_TYPE_TAG:
return "UTF_STRING";
case PRINTABLE_STRING_TYPE_TAG:
return "PRINTABLE_STRING";
case T61_STRING_TYPE_TAG:
return "T61_STRING";
case IA5_STRING_TYPE_TAG:
return "IA5_STRING";
case VISIBLE_STRING_TYPE_TAG:
return "VISIBLE_STRING";
case GENERAL_STRING_TYPE_TAG:
return "GENERAL_STRING";
case UNIVERSAL_STRING_TYPE_TAG:
return "UNIVERSAL_STRING";
case BMP_STRING_TYPE_TAG:
return "BMP_STRING";
case UTC_TIME_TYPE_TAG:
return "UTC_TIME";
case GENERALIZED_TIME_TYPE_TAG:
return "GENERAL_TIME";
}
} else {
switch (tag & 0x1F) {
case ENUMERATED_TYPE_TAG:
return "ENUMERATED";
case SEQUENCE_TYPE_TAG:
return "SEQUENCE";
case SET_TYPE_TAG:
return "SET";
}
}
}
return "'0x" + Hex.intToHexString(tag) + "'";
}
private static boolean isPrimitive(int tag) {
int i = 3;
for (; i >= 0; i--) {
int mask = (0xFF << (8 * i));
if ((tag & mask) != 0x00) { break; }
}
int msByte = (((tag & (0xFF << (8 * i))) >> (8 * i)) & 0xFF);
boolean result = ((msByte & 0x20) == 0x00);
return result;
}
public static int getTagLength(int tag) {
return getTagAsBytes(tag).length;
}
public static int getLengthLength(int length) {
return getLengthAsBytes(length).length;
}
/**
* The tag bytes of this object.
*
* @return the tag bytes of this object.
*/
public static byte[] getTagAsBytes(int tag) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
int byteCount = (int)(Math.log(tag) / Math.log(256)) + 1;
for (int i = 0; i < byteCount; i++) {
int pos = 8 * (byteCount - i - 1);
out.write((tag & (0xFF << pos)) >> pos);
}
byte[] tagBytes = out.toByteArray();
switch (getTagClass(tag)) {
case APPLICATION_CLASS:
tagBytes[0] |= 0x40;
break;
case CONTEXT_SPECIFIC_CLASS:
tagBytes[0] |= 0x80;
break;
case PRIVATE_CLASS:
tagBytes[0] |= 0xC0;
break;
}
if (!isPrimitive(tag)) {
tagBytes[0] |= 0x20;
}
return tagBytes;
}
/**
* The length bytes of this object.
*
* @return length of encoded value as bytes
*/
public static byte[] getLengthAsBytes(int length) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
if (length < 0x80) {
/* short form */
out.write(length);
} else {
int byteCount = log(length, 256);
out.write(0x80 | byteCount);
for (int i = 0; i < byteCount; i++) {
int pos = 8 * (byteCount - i - 1);
out.write((length & (0xFF << pos)) >> pos);
}
}
return out.toByteArray();
}
private static int log(int n, int base) {
int result = 0;
while (n > 0) {
n = n / base;
result ++;
}
return result;
}
/**
* The value of this object as a byte array. Almost the same as getEncoded(),
* but this one skips the tag and length of this
BERTLVObject.
*
* @return the value of this object as a byte array
*/
private static byte[] getValueAsBytes(int tag, Object value) {
if (value == null) {
System.out.println("DEBUG: object has no value: tag == "
+ Integer.toHexString(tag));
}
if (isPrimitive(tag)) {
if (value instanceof byte[]) {
return (byte[])value;
} else if (value instanceof String) {
return ((String)value).getBytes();
} else if (value instanceof Date) {
return SDF.format((Date)value).getBytes();
} else if (value instanceof Integer) {
int intValue = ((Integer)value).intValue();
int byteCount = Integer.bitCount(intValue) / 8 + 1;
byte[] result = new byte[byteCount];
for (int i = 0; i < byteCount; i++) {
int pos = 8 * (byteCount - i - 1);
result[i] = (byte)((intValue & (0xFF << pos)) >> pos);
}
return result;
} else if (value instanceof Byte) {
byte[] result = new byte[1];
result[0] = ((Byte)value).byteValue();
return result;
}
}
if (value instanceof BERTLVObject[]) {
ByteArrayOutputStream result = new ByteArrayOutputStream();
BERTLVObject[] children = (BERTLVObject[])value;
for (int i = 0; i < children.length; i++) {
try {
result.write(children[i].getEncoded());
} catch (IOException e) {
e.printStackTrace();
}
}
return result.toByteArray();
}
/* NOTE: Not primitive, and also not instance of BERTLVObject[]... */
if (value instanceof byte[]) {
// DSS-418: We are only using this method for getting the value of the SOD
// without interpreting it so a warning on STDERR is not apropriate thus
// it is commented out.
//System.err.println("DEBUG: WARNING: BERTLVobject with non-primitive tag "
// + Hex.intToHexString(tag) + " has byte[] value");
return (byte[])value;
}
throw new IllegalStateException("Cannot decode value of "
+ (value == null ? "null" : value.getClass().getCanonicalName())
+ " (tag = " + Hex.intToHexString(tag) + ")");
}
}