// ASN.1 JavaScript decoder // Copyright (c) 2008-2018 Lapo Luchini // Copyright (c) 2019-2019 Markus Hadenfeldt // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. declare class Int10 { constructor(value?: any); sub(sub: number); mulAdd(mul: number, add: number); simplify(); } const ellipsis = "\u2026"; function string_cut(str, len) { if (str.length > len) str = str.substring(0, len) + ellipsis; return str; } export class Stream { private static HEX_DIGITS = "0123456789ABCDEF"; private static reTimeS = /^(\d\d)(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([01]\d|2[0-3])(?:([0-5]\d)(?:([0-5]\d)(?:[.,](\d{1,3}))?)?)?(Z|[-+](?:[0]\d|1[0-2])([0-5]\d)?)?$/; private static reTimeL = /^(\d\d\d\d)(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([01]\d|2[0-3])(?:([0-5]\d)(?:([0-5]\d)(?:[.,](\d{1,3}))?)?)?(Z|[-+](?:[0]\d|1[0-2])([0-5]\d)?)?$/; position: number; data: string | ArrayBuffer; constructor(data: string | Stream | ArrayBuffer, position: number) { if (data instanceof Stream) this.data = data.data; else this.data = data; this.position = position; } length() : number { if (this.data instanceof ArrayBuffer) return this.data.byteLength; return this.data.length; } get(position?: number) { if (position === undefined) position = this.position++; if (position >= this.length()) throw 'Requesting byte offset ' + this.position + ' on a stream of length ' + this.length(); return (typeof(this.data) === "string") ? this.data.charCodeAt(position) : this.data[position]; } hexByte(byte: number) { return Stream.HEX_DIGITS.charAt((byte >> 4) & 0xF) + Stream.HEX_DIGITS.charAt(byte & 0xF); } parseStringISO(start, end) { let s = ""; for (let i = start; i < end; ++i) s += String.fromCharCode(this.get(i)); return s; } parseStringUTF(start, end) { let s = ""; for (let i = start; i < end;) { let c = this.get(i++); if (c < 128) s += String.fromCharCode(c); else if ((c > 191) && (c < 224)) s += String.fromCharCode(((c & 0x1F) << 6) | (this.get(i++) & 0x3F)); else s += String.fromCharCode(((c & 0x0F) << 12) | ((this.get(i++) & 0x3F) << 6) | (this.get(i++) & 0x3F)); } return s; } parseStringBMP(start, end) { let str = "", hi, lo; for (let i = start; i < end;) { hi = this.get(i++); lo = this.get(i++); str += String.fromCharCode((hi << 8) | lo); } return str; } parseTime(start, end, shortYear) { let s = this.parseStringISO(start, end), m = (shortYear ? Stream.reTimeS : Stream.reTimeL).exec(s); if (!m) return "Unrecognized time: " + s; if (shortYear) { // to avoid querying the timer, use the fixed range [1970, 2069] // it will conform with ITU X.400 [-10, +40] sliding window until 2030 //m[1] = +m[1]; //m[1] += (parseInt(m[1]) < 70) ? 2000 : 1900; throw "fixme!"; } s = m[1] + "-" + m[2] + "-" + m[3] + " " + m[4]; if (m[5]) { s += ":" + m[5]; if (m[6]) { s += ":" + m[6]; if (m[7]) s += "." + m[7]; } } if (m[8]) { s += " UTC"; if (m[8] != 'Z') { s += m[8]; if (m[9]) s += ":" + m[9]; } } return s; }; parseInteger(start, end) { let current: number = this.get(start); let negative = (current > 127); let padding = negative ? 255 : 0; let length; let descriptor: number | string; // skip unuseful bits (not allowed in DER) while (current == padding && ++start < end) current = this.get(start); length = end - start; if (length === 0) return negative ? '-1' : '0'; // show bit length of huge integers if (length > 4) { descriptor = current; length <<= 3; /* calculate bit length */ while (((descriptor ^ padding) & 0x80) == 0) { descriptor <<= 1; --length; } descriptor = "(" + length + " bit)\n"; } // decode the integer if (negative) current = current - 256; let number = ""; if(typeof(Int10) !== "undefined") { let n = new Int10(current); for (let i = start + 1; i < end; ++i) n.mulAdd(256, this.get(i)); number = n.toString(); } else { let n = 0; for (let i = start + 1; i < end; ++i) { n <<= 8; n += this.get(i); } number = n.toString(); } return descriptor + number; }; isASCII(start: number, end: number) { for (let i = start; i < end; ++i) { const c = this.get(i); if (c < 32 || c > 176) return false; } return true; }; parseBitString(start, end, maxLength) { let unusedBit = this.get(start), lenBit = ((end - start - 1) << 3) - unusedBit, intro = "(" + lenBit + " bit)\n", s = ""; for (let i = start + 1; i < end; ++i) { let b = this.get(i), skip = (i == end - 1) ? unusedBit : 0; for (let j = 7; j >= skip; --j) s += (b >> j) & 1 ? "1" : "0"; if (s.length > maxLength) return intro + string_cut(s, maxLength); } return intro + s; }; parseOctetString(start, end, maxLength) { if (this.isASCII(start, end)) return string_cut(this.parseStringISO(start, end), maxLength); let len = end - start, s = "(" + len + " byte)\n"; maxLength /= 2; // we work in bytes if (len > maxLength) end = start + maxLength; for (let i = start; i < end; ++i) s += this.hexByte(this.get(i)); if (len > maxLength) s += ellipsis; return s; }; parseOID(start, end, maxLength) { let s = '', n = new Int10(), bits = 0; for (let i = start; i < end; ++i) { let v = this.get(i); n.mulAdd(128, v & 0x7F); bits += 7; if (!(v & 0x80)) { // finished if (s === '') { n = n.simplify(); if (n instanceof Int10) { n.sub(80); s = "2." + n.toString(); } else { let m = n < 80 ? n < 40 ? 0 : 1 : 2; s = m + "." + (n - m * 40); } } else s += "." + n.toString(); if (s.length > maxLength) return string_cut(s, maxLength); n = new Int10(); bits = 0; } } if (bits > 0) s += ".incomplete"; /* FIXME if (typeof oids === 'object') { let oid = oids[s]; if (oid) { if (oid.d) s += "\n" + oid.d; if (oid.c) s += "\n" + oid.c; if (oid.w) s += "\n(warning!)"; } } */ return s; }; } export enum TagClass { UNIVERSAL = 0x00, APPLICATION = 0x01, CONTEXT = 0x02, PRIVATE = 0x03 } export enum TagType { EOC = 0x00, BOOLEAN = 0x01, INTEGER = 0x02, BIT_STRING = 0x03, OCTET_STRING = 0x04, NULL = 0x05, OBJECT_IDENTIFIER = 0x06, ObjectDescriptor = 0x07, EXTERNAL = 0x08, REAL = 0x09, ENUMERATED = 0x0A, EMBEDDED_PDV = 0x0B, UTF8String = 0x0C, SEQUENCE = 0x10, SET = 0x11, NumericString = 0x12, PrintableString = 0x13, // ASCII subset TeletextString = 0x14, // aka T61String VideotexString = 0x15, IA5String = 0x16, // ASCII UTCTime = 0x17, GeneralizedTime = 0x18, GraphicString = 0x19, VisibleString = 0x1A, // ASCII subset GeneralString = 0x1B, UniversalString = 0x1C, BMPString = 0x1E } class ASN1Tag { tagClass: TagClass; type: TagType; tagConstructed: boolean; tagNumber: number; constructor(stream: Stream) { let buf = stream.get(); this.tagClass = buf >> 6; this.tagConstructed = ((buf & 0x20) !== 0); this.tagNumber = buf & 0x1F; if (this.tagNumber == 0x1F) { // long tag let n = new Int10(); do { buf = stream.get(); n.mulAdd(128, buf & 0x7F); } while (buf & 0x80); this.tagNumber = n.simplify(); } } isUniversal() { return this.tagClass === 0x00; }; isEOC() { return this.tagClass === 0x00 && this.tagNumber === 0x00; }; } export class ASN1 { stream: Stream; header: number; length: number; tag: ASN1Tag; children: ASN1[]; constructor(stream: Stream, header: number, length: number, tag: ASN1Tag, children: ASN1[]) { this.stream = stream; this.header = header; this.length = length; this.tag = tag; this.children = children; } content(max_length?: number, type?: TagType) { // a preview of the content (intended for humans) if (this.tag === undefined) return null; if (max_length === undefined) max_length = Infinity; let content = this.posContent(), len = Math.abs(this.length); if (!this.tag.isUniversal()) { if (this.children !== null) return "(" + this.children.length + " elem)"; return this.stream.parseOctetString(content, content + len, max_length); } switch (type || this.tag.tagNumber) { case 0x01: // BOOLEAN return (this.stream.get(content) === 0) ? "false" : "true"; case 0x02: // INTEGER return this.stream.parseInteger(content, content + len); case 0x03: // BIT_STRING return this.children ? "(" + this.children.length + " elem)" : this.stream.parseBitString(content, content + len, max_length); case 0x04: // OCTET_STRING return this.children ? "(" + this.children.length + " elem)" : this.stream.parseOctetString(content, content + len, max_length); //case 0x05: // NULL case 0x06: // OBJECT_IDENTIFIER return this.stream.parseOID(content, content + len, max_length); //case 0x07: // ObjectDescriptor //case 0x08: // EXTERNAL //case 0x09: // REAL //case 0x0A: // ENUMERATED //case 0x0B: // EMBEDDED_PDV case 0x10: // SEQUENCE case 0x11: // SET if (this.children !== null) return "(" + this.children.length + " elem)"; else return "(no elem)"; case 0x0C: // UTF8String return string_cut(this.stream.parseStringUTF(content, content + len), max_length); case 0x12: // NumericString case 0x13: // PrintableString case 0x14: // TeletexString case 0x15: // VideotexString case 0x16: // IA5String //case 0x19: // GraphicString case 0x1A: // VisibleString //case 0x1B: // GeneralString //case 0x1C: // UniversalString return string_cut(this.stream.parseStringISO(content, content + len), max_length); case 0x1E: // BMPString return string_cut(this.stream.parseStringBMP(content, content + len), max_length); case 0x17: // UTCTime case 0x18: // GeneralizedTime return this.stream.parseTime(content, content + len, (this.tag.tagNumber == 0x17)); } return null; }; typeName(): string { switch (this.tag.tagClass) { case 0: // universal return TagType[this.tag.tagNumber] || ("Universal_" + this.tag.tagNumber.toString()); case 1: return "Application_" + this.tag.tagNumber.toString(); case 2: return "[" + this.tag.tagNumber.toString() + "]"; // Context case 3: return "Private_" + this.tag.tagNumber.toString(); } }; toString() { return this.typeName() + "@" + this.stream.position + "[header:" + this.header + ",length:" + this.length + ",sub:" + ((this.children === null) ? 'null' : this.children.length) + "]"; } toPrettyString(indent) { if (indent === undefined) indent = ''; let s = indent + this.typeName() + " @" + this.stream.position; if (this.length >= 0) s += "+"; s += this.length; if (this.tag.tagConstructed) s += " (constructed)"; else if ((this.tag.isUniversal() && ((this.tag.tagNumber == 0x03) || (this.tag.tagNumber == 0x04))) && (this.children !== null)) s += " (encapsulates)"; let content = this.content(); if (content) s += ": " + content.replace(/\n/g, '|'); s += "\n"; if (this.children !== null) { indent += ' '; for (let i = 0, max = this.children.length; i < max; ++i) s += this.children[i].toPrettyString(indent); } return s; }; posStart() { return this.stream.position; }; posContent() { return this.stream.position + this.header; }; posEnd() { return this.stream.position + this.header + Math.abs(this.length); }; static decodeLength(stream: Stream) { let buf = stream.get(); const len = buf & 0x7F; if (len == buf) return len; if (len > 6) // no reason to use Int10, as it would be a huge buffer anyways throw "Length over 48 bits not supported at position " + (stream.position - 1); if (len === 0) return null; // undefined buf = 0; for (let i = 0; i < len; ++i) buf = (buf << 8) + stream.get(); return buf; }; static encodeLength(buffer: Uint8Array, offset: number, length: number) { if(length < 0x7F) { buffer[offset] = length; } else { buffer[offset] = 0x80; let index = 1; while(length > 0) { buffer[offset + index++] = length & 0xFF; length >>= 8; buffer[offset] += 1; } } } } function decode0(stream: Stream) { const streamStart = new Stream(stream, 0); /* copy */ const tag = new ASN1Tag(stream); let len = ASN1.decodeLength(stream); const start = stream.position; const length_header = start - streamStart.position; let children = null; const query_children = () => { children = []; if (len !== null) { const end = start + len; if (end > stream.length()) throw 'Container at offset ' + start + ' has a length of ' + len + ', which is past the end of the stream'; while (stream.position < end) children[children.length] = decode0(stream); if (stream.position != end) throw 'Content size is not correct for container at offset ' + start; } else { // undefined length try { while (true) { const s = decode0(stream); if (s.tag.isEOC()) break; children[children.length] = s; } len = start - stream.position; // undefined lengths are represented as negative values } catch (e) { throw 'Exception while decoding undefined length content at offset ' + start + ': ' + e; } } }; if (tag.tagConstructed) { // must have valid content query_children(); } else if (tag.isUniversal() && ((tag.tagNumber == 0x03) || (tag.tagNumber == 0x04))) { // sometimes BitString and OctetString are used to encapsulate ASN.1 try { if (tag.tagNumber == 0x03) if (stream.get() != 0) throw "BIT STRINGs with unused bits cannot encapsulate."; query_children(); for (let i = 0; i < children.length; ++i) if (children[i].tag.isEOC()) throw 'EOC is not supposed to be actual content.'; } catch (e) { // but silently ignore when they don't children = null; //DEBUG console.log('Could not decode structure at ' + start + ':', e); } } if (children === null) { if (len === null) throw "We can't skip over an invalid tag with undefined length at offset " + start; stream.position = start + Math.abs(len); } return new ASN1(streamStart, length_header, len, tag, children); } export function decode(stream: string | ArrayBuffer) { return decode0(new Stream(stream, 0)); }