import java.io.*;
import java.util.*;
import java.text.*;
import java.nio.charset.StandardCharsets;
import java.math.BigInteger;
import java.util.function.Consumer;
public class Asn1Object implements Iterable<Asn1Object> {
public final static int UNIVERSAL = 0x00; // 0000 0000
public final static int APPLICATION = 0x40; // 0100 0000
public final static int CONTEXT = 0x80; // 1000 0000
public final static int PRIVATE = 0xC0; // 1100 0000
public final static int CONSTRUCTED = 0x20; // 0010 0000
public final static int EOC = 0x00;
public final static int BOOLEAN = 0x01;
public final static int INTEGER = 0x02;
public final static int BIT_STRING = 0x03;
public final static int OCTET_STRING = 0x04;
public final static int NULL = 0x05;
public final static int OBJECT_IDENTIFIER = 0x06;
public final static int OBJECT_DESCRIPTOR = 0x07;
public final static int EXTERNAL = 0x08;
public final static int REAL = 0x09;
public final static int ENUMERATED = 0x0A;
public final static int EMBEDDED_PDV = 0x0B;
public final static int UTF8_STRING = 0x0C;
public final static int SEQUENCE = 0x10;
public final static int SET = 0x11;
public final static int NUMERIC_STRING = 0x12;
public final static int PRINTABLE_STRING = 0x13;
public final static int T61_STRING = 0x14;
public final static int VIDEOTEX_STRING = 0x15;
public final static int IA5_STRING = 0x16;
public final static int UTC_TIME = 0x17;
public final static int GENERALIZED_TIME = 0x18;
public final static int GRAPHIC_STRING = 0x19;
public final static int VISIBLE_STRING = 0x1A;
public final static int GENERAL_STRING = 0x1B;
public final static int UNIVERSAL_STRING = 0x1C;
public final static int CHARACTER_STRING = 0x1D;
public final static int BMP_STRING = 0x1E; // 0001 1110
private boolean indefinite = false, encapsulated = false;
private int flags, tag, length = 0;
private byte[] bytes = null;
private List<Asn1Object> subElements = null;
private Asn1Object parent = null;
public Asn1Object(InputStream in) {
try {
read(in);
} catch (Exception e) { throw new RuntimeException(e); }
}
public Asn1Object(byte[] bytes) {
this(new ByteArrayInputStream(bytes));
if (headLength() + length != bytes.length) throw new IllegalArgumentException("fragment found");
}
public Asn1Object(int flags, int tag) {
if ((flags & 0xE0) != flags) throw new IllegalArgumentException("flags"); // 1110 0000
this.flags = flags;
this.tag = tag;
}
public Asn1Object(int flags, int tag, Object obj) {
this(flags, tag, toBytes(tag, obj));
}
public Asn1Object(int flags, int tag, byte[] bytes) {
this(flags, tag);
this.bytes = (isConstructed() ? null : bytes);
this.length = (isConstructed() || bytes == null ? 0 : bytes.length);
checkEncapsulated();
}
public Asn1Object() {
this(UNIVERSAL, EOC);
}
public boolean isConstructed() { return (flags & CONSTRUCTED) != 0; }
public boolean isEncapsulated() { return encapsulated; }
public boolean isIndefinite() { return indefinite; }
public Asn1Object setIndefinite(boolean indefinite) {
this.indefinite = isConstructed() && indefinite;
return this;
}
public int getFlags() { return flags; }
public int getTag() { return tag; }
public int getLength() { return length; }
public byte[] getValue() { return bytes; }
public Asn1Object getParent() { return parent; }
public int headLength() { return tagLength() + lengthLength(); }
public int tagLength() { return tag < 31 ? 1 : getNum(tag, 7) + 1; }
public int lengthLength() { return length < 128 || indefinite ? 1 : getNum(length, 8) + 1; }
public Asn1Object getSubElement(int i) { return subElements.get(i); }
public int getSubElementsCount() { return subElements == null ? 0 : subElements.size(); }
public Iterator<Asn1Object> getSubElementsIterator() {
return subElements == null ? Collections.<Asn1Object>emptyIterator() : subElements.iterator();
}
public Asn1Object addAll(Asn1Object... elements) {
for (Asn1Object o : elements) add(o);
return this;
}
public void add(Asn1Object o) {
if (o == null) return;
if (!isConstructed() && !isEncapsulated()) throw new RuntimeException("addition in non constructed or encapsulated");
//if (!isIndefinite() && o.getFlags() == 0 && o.getTag() == EOC) throw new RuntimeException("addition EOC in non indefinite");
if (subElements == null) subElements = new ArrayList<>();
subElements.add(o);
o.parent = this;
if (!isEncapsulated()) {
int l = o.headLength() + o.getLength();
length += l;
Asn1Object parent = getParent();
while (parent != null && !parent.isEncapsulated()) {
parent.length += l;
parent = parent.getParent();
}
}
}
private static int getNum(long value, int bits) {
int size = 1;
while ((value >>>= bits) != 0) size++;
return size;
}
private void readTag(InputStream in) throws IOException {
int data = in.read();
if (data == -1) throw new EOFException("EOF found");
flags = data & 0xE0; // 1110 0000
tag = data & 0x1F; // 0001 1111
if (tag == 0x1F) {
tag = 0;
int b = in.read();
if (b == -1) throw new EOFException("EOF found");
if ((b & 0x7f) == 0) throw new IOException("invalid tag number");
while ((b & 0x80) != 0) {
tag |= (b & 0x7f); // 0111 1111
tag <<= 7;
b = in.read();
if (b == -1) throw new EOFException("EOF found");
}
tag |= (b & 0x7f);
}
}
private void writeTag(OutputStream out) throws IOException {
if (tag < 31) { // 0x1f 0001 1111
out.write((byte)(flags | tag));
} else {
out.write((byte)(flags | 0x1f));
if (tag < 128) { // 0x80 1000 0000
out.write((byte)tag);
} else {
int num = getNum(tag, 7);
for (int i = (num - 1) * 7; i >= 0; i -= 7) {
out.write((byte)(i > 0 ? (tag >>> i) | 0x80 : (tag >>> i) & 0x7F));
}
}
}
}
private void readLength(InputStream in) throws IOException {
length = in.read();
if (length == -1) throw new EOFException("EOF found");
if (length == 128) { // 0x80 1000 0000 indefinite length encoding
if (!isConstructed()) throw new IOException("tag can't be indefinite");
length = 0;
indefinite = true;
} else if (length > 127) { //0x7F 0111 1111
int num = length & 0x7F;
if (num > 4) throw new IOException("length more than 4 bytes");
length = 0;
for (int i = 0; i < num; i++) {
int b = in.read();
if (b == -1) throw new EOFException("EOF found");
if (i == 0 && b == 0) throw new IOException("invalid tag length");
length = (length << 8) | (b & 0xFF);
}
}
}
private void writeLength(OutputStream out) throws IOException {
if (isConstructed() && indefinite) {
out.write((byte)0x80); // 1000 0000
} else {
if (length > 127) { // 0x7F 0111 1111
int num = getNum(length, 8);
out.write((byte)(num | 0x80)); // 1000 0000
for (int i = (num - 1) * 8; i >= 0; i -= 8) {
out.write((byte)(length >>> i));
}
} else {
out.write((byte)length);
}
}
}
public int effHeadLength() {
int len = headLength();
if (!isConstructed()) {
if (isEncapsulated()) len += prefEncapsulated().length;
else len += getLength();
}
return len;
}
public Iterator<Asn1Object> iterator() {
return new Asn1Iterator(this);
}
public void read(InputStream in) throws IOException {
readTag(in);
readLength(in);
if (isConstructed() && (length > 0 || indefinite)) {
int l = length;
length = 0;
for (;;) {
Asn1Object so = new Asn1Object(in);
add(so);
if (!indefinite) {
if (length == l) break;
if (length > l) throw new IOException("invalid summary length");
} else if (so.getFlags() == 0 && so.getTag() == EOC && so.length == 0) break;
}
} else if (length > 0) {
bytes = new byte[length];
int n = in.read(bytes);
if (n != length) throw new EOFException("length " + length + " found " + n);
checkEncapsulated();
}
}
public boolean allowEncapsulated() { return (flags & 0xC0) == UNIVERSAL && (tag == BIT_STRING || tag == OCTET_STRING); }
private void checkEncapsulated() {
encapsulated = false;
if(!isConstructed() && subElements != null) subElements.clear();
if (!allowEncapsulated() || bytes == null || bytes.length < 2) return;
byte[] bt;
if (tag == BIT_STRING) {
if (bytes[0] != 0) return;
bt = new byte[bytes.length - prefEncapsulated().length];
System.arraycopy(bytes, 1, bt, 0, bt.length);
} else bt = bytes;
try {
Asn1Object e = new Asn1Object(bt);
if (!(e.isConstructed() || e.allowEncapsulated()) || e.isIndefinite()) return;
encapsulated = true;
add(e);
} catch (Exception e) {}
}
public byte[] prefEncapsulated() {
if (allowEncapsulated() && tag == BIT_STRING) return new byte[] { 0 };
return new byte[0];
}
public void write(OutputStream out) throws IOException {
for (Asn1Object o : this) o.writeObj(out);
}
public void writeObj(OutputStream out) throws IOException {
if (isIndefinite() && isConstructed() &&
(getSubElementsCount() == 0 ||
(getSubElementsCount() > 0 && getSubElement(getSubElementsCount() - 1).getTag() != EOC)))
throw new IOException("EOC missing");
writeTag(out);
writeLength(out);
if (!isConstructed()) {
if (isEncapsulated()) out.write(prefEncapsulated());
else if (bytes != null) {
if (bytes.length != length) throw new IOException("byte array length incorrect");
out.write(bytes);
}
}
}
public byte[] getBytes() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
write(out);
} catch (Exception e) { throw new RuntimeException(e); }
byte[] bytes = out.toByteArray();
return bytes;
}
@Override
public Asn1Object clone() {
return new Asn1Object(getBytes());
}
@Override
public boolean equals(Object obj) {
return (obj instanceof Asn1Object && Arrays.equals(getBytes(), ((Asn1Object)obj).getBytes()));
}
@Override
public int hashCode() {
return Arrays.hashCode(getBytes());
}
public final static int STDEFAULT = 0;
public final static int STPREFIX = 1;
public final static int STVALUE = 2;
public String valueAsString() { return valueAsString(STDEFAULT); }
public String valueAsString(int t) {
String pref = "", ret = "";
Object obj = valueAsObject();
if (obj != null) {
if (t != STVALUE) {
if (tag == BIT_STRING && !isEncapsulated() && obj instanceof String) pref = "("+((String)obj).length()+" bit)";
//else if (tag == OCTET_STRING && !isEncapsulated() && obj instanceof String) pref = "("+((String)obj).length() / 2+" byte)";
}
if (t != STPREFIX) {
if (obj instanceof Date) ret = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(obj);
else ret = obj.toString();
}
}
return pref + (pref.length() > 0 && ret.length() > 0 ? " " : "") + ret;
}
public Object valueAsObject() {
if (isConstructed() || isEncapsulated() || bytes == null) return null;
try {
if ((flags & 0xC0) == UNIVERSAL) {
if (tag == UTF8_STRING) return new String(bytes, StandardCharsets.UTF_8);
if (tag == NUMERIC_STRING || tag == PRINTABLE_STRING || tag == T61_STRING ||
tag == VIDEOTEX_STRING || tag == IA5_STRING || tag == VISIBLE_STRING)
return new String(bytes, StandardCharsets.ISO_8859_1);
if (tag == BMP_STRING) return new String(bytes, StandardCharsets.UTF_16BE);
if (tag == BOOLEAN) return Boolean.valueOf(bytes[0] != 0);
if ((tag == UTC_TIME || tag == GENERALIZED_TIME) && bytes[bytes.length - 1] == 'Z') {
String f = (tag == GENERALIZED_TIME ? "yy" : "") + "yyMMddHHmmss";
String s = new String(bytes, StandardCharsets.UTF_8);
return new SimpleDateFormat(f).parse(s);
}
if (tag == INTEGER) return new BigInteger(bytes);
if (tag == BIT_STRING && bytes.length > 0) {
int l = (int)bytes[0] & 0xff;
StringBuilder sb = new StringBuilder();
for (int i = 8; i < bytes.length * 8 - l; i++) {
sb.append(((bytes[i / 8] << (i % 8)) & 0x80) > 0 ? "1" : "0");
}
return sb.toString();
}
if (tag == OBJECT_IDENTIFIER) {
int count = 0;
for (byte b : bytes) if ((b & 0x80) == 0) count++;
long[] data = new long[count+1];
for (int d = 1, i = 0; i < bytes.length; i++) {
data[d] = (data[d] << 7) | (bytes[i] & 0x7f);
if ((bytes[i] & 0x80) == 0) d++;
}
data[0] = data[1] < 80 ? data[1] < 40 ? 0 : 1 : 2;
data[1] = data[1] - data[0] * 40;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < data.length; i++) {
if (data[i] < 0) throw new IllegalArgumentException();
if (i > 0) sb.append(".");
sb.append(Long.toString(data[i]));
}
return sb.toString();
}
}
StringBuilder sb = new StringBuilder();
for (byte b : bytes) sb.append(String.format("%02X", b));
return sb.toString();
} catch (Exception e) {}
return null;
}
public static byte[] toBytes(int tag, Object obj) {
if (obj instanceof String) {
String string = (String)obj;
if (tag == UTF8_STRING) return string.getBytes(StandardCharsets.UTF_8);
if (tag == NUMERIC_STRING || tag == PRINTABLE_STRING || tag == T61_STRING ||
tag == VIDEOTEX_STRING || tag == IA5_STRING || tag == VISIBLE_STRING)
return string.getBytes(StandardCharsets.ISO_8859_1);
if (tag == BMP_STRING) return string.getBytes(StandardCharsets.UTF_16BE);
if (tag == OBJECT_IDENTIFIER) {
String[] s = string.split("\\.");
long[] l = new long[s.length];
for (int i = 0; i < s.length; i++) {
l[i] = Long.parseLong(s[i]);
if (l[i] < 0) throw new IllegalArgumentException();
}
if ((l.length < 2) || (l[0] > 2) || (l[0] < 2 && l[1] >= 40)) throw new IllegalArgumentException();
l[1] += l[0] * 40;
int sum = 0, pos = 0;
for (int i = 1; i < l.length; i++) sum += getNum(l[i], 7);
byte[] bytes = new byte[sum];
for (int i = 1; i < l.length; i++) {
int num = getNum(l[i], 7);
for (int j = (num - 1) * 7; j >= 0; j -= 7) {
bytes[pos++] = ((byte)(j > 0 ? (l[i] >>> j) | 0x80 : (l[i] >>> j) & 0x7F));
}
}
return bytes;
}
} else if (obj instanceof Boolean && tag == BOOLEAN) {
return new byte[] { (byte)(((Boolean)obj).booleanValue() ? 0xFF : 0x00) };
} else if (obj instanceof Date) {
String f = null;
if (tag == UTC_TIME) f = "yyMMddHHmmss'Z'";
else if (tag == GENERALIZED_TIME) f = "yyyyMMddHHmmss'Z'";
if (f != null) {
SimpleDateFormat dateFormat = new SimpleDateFormat(f);
//dateFormat.setTimeZone(new SimpleTimeZone(0, "Z"));
return dateFormat.format((Date)obj).getBytes(StandardCharsets.UTF_8);
}
} else if (obj instanceof Object[]) {
Object[] array = ((Object[])obj);
if (tag == BIT_STRING && array.length == 2 && array[0] instanceof Integer && array[1] instanceof byte[]) {
//if (((Integer)array[0]).intValue() > 7) throw new IllegalArgumentException();
byte[] bytes = new byte[((byte[])array[1]).length + 1];
System.arraycopy((byte[])array[1], 0, bytes, 1, ((byte[])array[1]).length);
bytes[0] = ((Integer)array[0]).byteValue();
return bytes;
}
} else if (obj instanceof Integer && tag == INTEGER) {
return new BigInteger(((Integer)obj).toString()).toByteArray();
} else if (obj instanceof Long && tag == INTEGER) {
return new BigInteger(((Long)obj).toString()).toByteArray();
} else if (obj instanceof BigInteger && tag == INTEGER) {
return ((BigInteger)obj).toByteArray();
}
throw new IllegalArgumentException("object not defined");
}
public String typeName() {
switch (flags & 0xC0) { // 1100 0000
case UNIVERSAL:
switch (tag) {
case EOC: return "EOC";
case BOOLEAN: return "BOOLEAN";
case INTEGER: return "INTEGER";
case BIT_STRING: return "BIT STRING";
case OCTET_STRING: return "OCTET STRING";
case NULL: return "NULL";
case OBJECT_IDENTIFIER: return "OBJECT IDENTIFIER";
case OBJECT_DESCRIPTOR: return "OBJECT DESCRIPTOR";
case EXTERNAL: return "EXTERNAL";
case REAL: return "REAL";
case ENUMERATED: return "ENUMERATED";
case EMBEDDED_PDV: return "EMBEDDED PDV";
case UTF8_STRING: return "UTF8 STRING";
case SEQUENCE: return "SEQUENCE";
case SET: return "SET";
case NUMERIC_STRING: return "NUMERIC STRING";
case PRINTABLE_STRING: return "PRINTABLE STRING";
case T61_STRING: return "T61 STRING";
case VIDEOTEX_STRING: return "VIDEOTEX STRING";
case IA5_STRING: return "IA5 STRING";
case UTC_TIME: return "UTC TIME";
case GENERALIZED_TIME: return "GENERALIZED TIME";
case GRAPHIC_STRING: return "GRAPHIC STRING";
case VISIBLE_STRING: return "VISIBLE STRING"; // ISO646_STRING
case GENERAL_STRING: return "GENERAL STRING";
case UNIVERSAL_STRING: return "UNIVERSAL STRING";
case CHARACTER_STRING: return "CHARACTER STRING";
case BMP_STRING: return "BMP STRING";
}
return "Universal_" + tag;
case APPLICATION: return "Application_" + tag;
case CONTEXT: return "Context_" + tag;
case PRIVATE: return "Private_" + tag;
}
return "unknown";
}
@Override
public String toString() {
return "[" + String.format("%02X", tag) + "] " + typeName() +
(isConstructed() ? " (" + getSubElementsCount() + " elem)" : (isEncapsulated() ? " (encapsulates "+getSubElementsCount()+" elem)":"")) +
" Length: " + headLength() + (isEncapsulated() && prefEncapsulated().length > 0 ? "+" + prefEncapsulated().length : "") + "+" + length +(indefinite ? " (indefinite)" : "");
}
public void print() { print(System.out::println); }
public void print(Consumer<String> action) {
int offset = 0;
for (Asn1Object o : this) {
StringBuilder sb = new StringBuilder();
Asn1Object parent = o.getParent();
while (parent != getParent()) {
sb.append(" ");
parent = parent.getParent();
}
sb.append(o.toString() + " Offset: " + offset);
String s = o.valueAsString();
if (s.length() > 0) {
if (s.length() > 30) s = s.substring(0, 27) + "...";
sb.append(" Value: " + s);
}
//sb.setLength(80);
action.accept(sb.toString());
offset += o.effHeadLength();
}
}
private class Asn1Iterator implements Iterator<Asn1Object> {
private final Stack<ListIterator<Asn1Object>> stack = new Stack<>();
private Asn1Object next;
public Asn1Iterator(Asn1Object root) { next = root; }
public boolean hasNext() { return (next != null); }
public Asn1Object next() {
if (!hasNext()) throw new NoSuchElementException();
Asn1Object o = next;
if (next.getSubElementsCount() > 0) {
stack.push(o.subElements.listIterator());
} else {
for (;;) {
if (stack.empty()) { next = null; return o; }
if (stack.peek().hasNext()) break;
stack.pop();
}
}
next = stack.peek().next();
return o;
}
} // End of class Asn1Iterator
} // End of class Asn1Object