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