// ASN.1 JavaScript decoder // Copyright (c) 2008-2014 Lapo Luchini // 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. /*jshint browser: true, strict: true, immed: true, latedef: true, undef: true, regexdash: false */ /*global oids */ (function (undefined) { "use strict"; var Int10 = (typeof module !== 'undefined') ? require('./int10.js') : window.Int10, ellipsis = "\u2026", reTime = /^((?:1[89]|2\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)?)?$/; function stringCut(str, len) { if (str.length > len) str = str.substring(0, len) + ellipsis; return str; } function Stream(enc, pos) { if (enc instanceof Stream) { this.enc = enc.enc; this.pos = enc.pos; } else { this.enc = enc; this.pos = pos; } } Stream.prototype.get = function (pos) { if (pos === undefined) pos = this.pos++; if (pos >= this.enc.length) throw 'Requesting byte offset ' + pos + ' on a stream of length ' + this.enc.length; return this.enc[pos]; }; Stream.prototype.hexDigits = "0123456789ABCDEF"; Stream.prototype.hexByte = function (b) { return this.hexDigits.charAt((b >> 4) & 0xF) + this.hexDigits.charAt(b & 0xF); }; Stream.prototype.hexDump = function (start, end, raw) { var s = ""; for (var i = start; i < end; ++i) { s += this.hexByte(this.get(i)); if (raw !== true) switch (i & 0xF) { case 0x7: s += " "; break; case 0xF: s += "\n"; break; default: s += " "; } } return s; }; Stream.prototype.isASCII = function (start, end) { for (var i = start; i < end; ++i) { var c = this.get(i); if (c < 32 || c > 176) return false; } return true; }; Stream.prototype.parseStringISO = function (start, end) { var s = ""; for (var i = start; i < end; ++i) s += String.fromCharCode(this.get(i)); return s; }; Stream.prototype.parseStringUTF = function (start, end) { var s = ""; for (var i = start; i < end; ) { var 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; }; Stream.prototype.parseStringBMP = function (start, end) { var str = "", hi, lo; for (var i = start; i < end; ) { hi = this.get(i++); lo = this.get(i++); str += String.fromCharCode((hi << 8) | lo); } return str; }; Stream.prototype.parseTime = function (start, end, shortYear) { var s = this.parseStringISO(start, end), m = reTime.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] += (m[1] < 70) ? 2000 : 1900; } 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; }; Stream.prototype.parseInteger = function (start, end) { var v = this.get(start), neg = (v > 127), pad = neg ? 255 : 0, len, s = ''; // skip unuseful bits (not allowed in DER) while (v == pad && start < end) v = this.get(++start); len = end - start; if (len === 0) return neg ? -1 : 0; // show bit length of huge integers if (len > 4) { s = v; len <<= 3; while (((s ^ pad) & 0x80) == 0) { s <<= 1; --len; } s = "(" + len + " bit)\n"; } // decode the integer if (neg) v = v - 256; var n = new Int10(v); for (var i = start + 1; i < end; ++i) n.mulAdd(256, this.get(i)); return s + n.toString(); }; Stream.prototype.parseBitString = function (start, end, maxLength) { var unusedBit = this.get(start), lenBit = ((end - start - 1) << 3) - unusedBit, intro = "(" + lenBit + " bit)\n", s = "", skip = unusedBit; for (var i = end - 1; i > start; --i) { var b = this.get(i); for (var j = skip; j < 8; ++j) s += (b >> j) & 1 ? "1" : "0"; skip = 0; if (s.length > maxLength) return intro + stringCut(s, maxLength); } return intro + s; }; Stream.prototype.parseOctetString = function (start, end, maxLength) { if (this.isASCII(start, end)) return stringCut(this.parseStringISO(start, end), maxLength); var len = end - start, s = "(" + len + " byte)\n"; maxLength /= 2; // we work in bytes if (len > maxLength) end = start + maxLength; for (var i = start; i < end; ++i) s += this.hexByte(this.get(i)); if (len > maxLength) s += ellipsis; return s; }; Stream.prototype.parseOID = function (start, end, maxLength) { var s = '', n = new Int10(), bits = 0; for (var i = start; i < end; ++i) { var v = this.get(i); n.mulAdd(128, v & 0x7F); bits += 7; if (!(v & 0x80)) { // finished if (s === '') { n = n.simplify(); var m = n < 80 ? n < 40 ? 0 : 1 : 2; s = m + "." + (n - m * 40); } else s += "." + n.toString(); if (s.length > maxLength) return stringCut(s, maxLength); n = new Int10(); bits = 0; } } if (bits > 0) s += ".incomplete"; return s; }; function ASN1(stream, header, length, tag, sub) { if (!(tag instanceof ASN1Tag)) throw 'Invalid tag value.'; this.stream = stream; this.header = header; this.length = length; this.tag = tag; this.sub = sub; } ASN1.prototype.typeName = function () { switch (this.tag.tagClass) { case 0: // universal switch (this.tag.tagNumber) { case 0x00: return "EOC"; case 0x01: return "BOOLEAN"; case 0x02: return "INTEGER"; case 0x03: return "BIT_STRING"; case 0x04: return "OCTET_STRING"; case 0x05: return "NULL"; case 0x06: return "OBJECT_IDENTIFIER"; case 0x07: return "ObjectDescriptor"; case 0x08: return "EXTERNAL"; case 0x09: return "REAL"; case 0x0A: return "ENUMERATED"; case 0x0B: return "EMBEDDED_PDV"; case 0x0C: return "UTF8String"; case 0x10: return "SEQUENCE"; case 0x11: return "SET"; case 0x12: return "NumericString"; case 0x13: return "PrintableString"; // ASCII subset case 0x14: return "TeletexString"; // aka T61String case 0x15: return "VideotexString"; case 0x16: return "IA5String"; // ASCII case 0x17: return "UTCTime"; case 0x18: return "GeneralizedTime"; case 0x19: return "GraphicString"; case 0x1A: return "VisibleString"; // ASCII subset case 0x1B: return "GeneralString"; case 0x1C: return "UniversalString"; case 0x1E: return "BMPString"; } return "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(); } }; ASN1.prototype.content = function (maxLength) { // a preview of the content (intended for humans) if (this.tag === undefined) return null; if (maxLength === undefined) maxLength = Infinity; var content = this.posContent(), len = Math.abs(this.length); if (!this.tag.isUniversal()) { if (this.sub !== null) return "(" + this.sub.length + " elem)"; return this.stream.parseOctetString(content, content + len, maxLength); } switch (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.sub ? "(" + this.sub.length + " elem)" : this.stream.parseBitString(content, content + len, maxLength); case 0x04: // OCTET_STRING return this.sub ? "(" + this.sub.length + " elem)" : this.stream.parseOctetString(content, content + len, maxLength); //case 0x05: // NULL case 0x06: // OBJECT_IDENTIFIER return this.stream.parseOID(content, content + len, maxLength); //case 0x07: // ObjectDescriptor //case 0x08: // EXTERNAL //case 0x09: // REAL //case 0x0A: // ENUMERATED //case 0x0B: // EMBEDDED_PDV case 0x10: // SEQUENCE case 0x11: // SET return "(" + this.sub.length + " elem)"; case 0x0C: // UTF8String return stringCut(this.stream.parseStringUTF(content, content + len), maxLength); 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 stringCut(this.stream.parseStringISO(content, content + len), maxLength); case 0x1E: // BMPString return stringCut(this.stream.parseStringBMP(content, content + len), maxLength); case 0x17: // UTCTime case 0x18: // GeneralizedTime return this.stream.parseTime(content, content + len, (this.tag.tagNumber == 0x17)); } return null; }; ASN1.prototype.toString = function () { return this.typeName() + "@" + this.stream.pos + "[header:" + this.header + ",length:" + this.length + ",sub:" + ((this.sub === null) ? 'null' : this.sub.length) + "]"; }; ASN1.prototype.toPrettyString = function (indent) { if (indent === undefined) indent = ''; var s = indent + this.typeName() + " @" + this.stream.pos; 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.sub !== null)) s += " (encapsulates)"; s += "\n"; if (this.sub !== null) { indent += ' '; for (var i = 0, max = this.sub.length; i < max; ++i) s += this.sub[i].toPrettyString(indent); } return s; }; ASN1.prototype.posStart = function () { return this.stream.pos; }; ASN1.prototype.posContent = function () { return this.stream.pos + this.header; }; ASN1.prototype.posEnd = function () { return this.stream.pos + this.header + Math.abs(this.length); }; ASN1.prototype.toHexString = function (root) { return this.stream.hexDump(this.posStart(), this.posEnd(), true); }; ASN1.decodeLength = function (stream) { var buf = stream.get(), 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.pos - 1); if (len === 0) return null; // undefined buf = 0; for (var i = 0; i < len; ++i) buf = (buf * 256) + stream.get(); return buf; }; function ASN1Tag(stream) { var buf = stream.get(); this.tagClass = buf >> 6; this.tagConstructed = ((buf & 0x20) !== 0); this.tagNumber = buf & 0x1F; if (this.tagNumber == 0x1F) { // long tag var n = new Int10(); do { buf = stream.get(); n.mulAdd(128, buf & 0x7F); } while (buf & 0x80); this.tagNumber = n.simplify(); } } ASN1Tag.prototype.isUniversal = function () { return this.tagClass === 0x00; }; ASN1Tag.prototype.isEOC = function () { return this.tagClass === 0x00 && this.tagNumber === 0x00; }; ASN1.decode = function (stream) { if (!(stream instanceof Stream)) stream = new Stream(stream, 0); var streamStart = new Stream(stream), tag = new ASN1Tag(stream), len = ASN1.decodeLength(stream), start = stream.pos, header = start - streamStart.pos, sub = null, getSub = function () { sub = []; if (len !== null) { // definite length var end = start + len; while (stream.pos < end) sub[sub.length] = ASN1.decode(stream); if (stream.pos != end) throw "Content size is not correct for container starting at offset " + start; } else { // undefined length try { for (;;) { var s = ASN1.decode(stream); if (s.tag.isEOC()) break; sub[sub.length] = s; } len = start - stream.pos; // undefined lengths are represented as negative values } catch (e) { throw "Exception while decoding undefined length content: " + e; } } }; if (tag.tagConstructed) { // must have valid content getSub(); } else if (tag.isUniversal() && ((tag.tagNumber == 0x03) || (tag.tagNumber == 0x04))) { if (tag.tagNumber == 0x03) stream.get(); // skip BitString unused bits, must be in [0, 7] // sometimes BitString and OctetString do contain ASN.1 try { getSub(); for (var i = 0; i < sub.length; ++i) if (sub[i].tag.isEOC()) throw 'EOC is not supposed to be actual content.'; } catch (e) { // but silently ignore when they don't sub = null; } } if (sub === null) { if (len === null) throw "We can't skip over an invalid tag with undefined length at offset " + start; stream.pos = start + Math.abs(len); } return new ASN1(streamStart, header, len, tag, sub); }; // export globals if (typeof module !== 'undefined') { module.exports = ASN1; } else { window.ASN1 = ASN1; } })();