blob: 069e02bcb1545a4d39a094df1adb53231c764332 [file] [log] [blame]
Wentao Shang882e34e2013-01-05 02:49:51 -08001// ASN.1 JavaScript decoder
2// Copyright (c) 2008-2009 Lapo Luchini <lapo@lapo.it>
3
4// Permission to use, copy, modify, and/or distribute this software for any
5// purpose with or without fee is hereby granted, provided that the above
6// copyright notice and this permission notice appear in all copies.
7//
8// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15
16function Stream(enc, pos) {
17 if (enc instanceof Stream) {
18 this.enc = enc.enc;
19 this.pos = enc.pos;
20 } else {
21 this.enc = enc;
22 this.pos = pos;
23 }
24}
25Stream.prototype.get = function(pos) {
26 if (pos == undefined)
27 pos = this.pos++;
28 if (pos >= this.enc.length)
29 throw 'Requesting byte offset ' + pos + ' on a stream of length ' + this.enc.length;
30 return this.enc[pos];
31}
32Stream.prototype.hexDigits = "0123456789ABCDEF";
33Stream.prototype.hexByte = function(b) {
34 return this.hexDigits.charAt((b >> 4) & 0xF) + this.hexDigits.charAt(b & 0xF);
35}
36Stream.prototype.hexDump = function(start, end) {
37 var s = "";
38 for (var i = start; i < end; ++i) {
39 s += this.hexByte(this.get(i));
40 switch (i & 0xF) {
41 case 0x7: s += " "; break;
42 case 0xF: s += "\n"; break;
43 default: s += " ";
44 }
45 }
46 return s;
47}
48Stream.prototype.parseStringISO = function(start, end) {
49 var s = "";
50 for (var i = start; i < end; ++i)
51 s += String.fromCharCode(this.get(i));
52 return s;
53}
54Stream.prototype.parseStringUTF = function(start, end) {
55 var s = "", c = 0;
56 for (var i = start; i < end; ) {
57 var c = this.get(i++);
58 if (c < 128)
59 s += String.fromCharCode(c);
60 else if ((c > 191) && (c < 224))
61 s += String.fromCharCode(((c & 0x1F) << 6) | (this.get(i++) & 0x3F));
62 else
63 s += String.fromCharCode(((c & 0x0F) << 12) | ((this.get(i++) & 0x3F) << 6) | (this.get(i++) & 0x3F));
64 //TODO: this doesn't check properly 'end', some char could begin before and end after
65 }
66 return s;
67}
68Stream.prototype.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)?)?$/;
69Stream.prototype.parseTime = function(start, end) {
70 var s = this.parseStringISO(start, end);
71 var m = this.reTime.exec(s);
72 if (!m)
73 return "Unrecognized time: " + s;
74 s = m[1] + "-" + m[2] + "-" + m[3] + " " + m[4];
75 if (m[5]) {
76 s += ":" + m[5];
77 if (m[6]) {
78 s += ":" + m[6];
79 if (m[7])
80 s += "." + m[7];
81 }
82 }
83 if (m[8]) {
84 s += " UTC";
85 if (m[8] != 'Z') {
86 s += m[8];
87 if (m[9])
88 s += ":" + m[9];
89 }
90 }
91 return s;
92}
93Stream.prototype.parseInteger = function(start, end) {
94 //TODO support negative numbers
95 var len = end - start;
96 if (len > 4) {
97 len <<= 3;
98 var s = this.get(start);
99 if (s == 0)
100 len -= 8;
101 else
102 while (s < 128) {
103 s <<= 1;
104 --len;
105 }
106 return "(" + len + " bit)";
107 }
108 var n = 0;
109 for (var i = start; i < end; ++i)
110 n = (n << 8) | this.get(i);
111 return n;
112}
113Stream.prototype.parseBitString = function(start, end) {
114 var unusedBit = this.get(start);
115 var lenBit = ((end - start - 1) << 3) - unusedBit;
116 var s = "(" + lenBit + " bit)";
117 if (lenBit <= 20) {
118 var skip = unusedBit;
119 s += " ";
120 for (var i = end - 1; i > start; --i) {
121 var b = this.get(i);
122 for (var j = skip; j < 8; ++j)
123 s += (b >> j) & 1 ? "1" : "0";
124 skip = 0;
125 }
126 }
127 return s;
128}
129Stream.prototype.parseOctetString = function(start, end) {
130 var len = end - start;
131 var s = "(" + len + " byte) ";
132 if (len > 20)
133 end = start + 20;
134 for (var i = start; i < end; ++i)
135 s += this.hexByte(this.get(i));
136 if (len > 20)
137 s += String.fromCharCode(8230); // ellipsis
138 return s;
139}
140Stream.prototype.parseOID = function(start, end) {
141 var s, n = 0, bits = 0;
142 for (var i = start; i < end; ++i) {
143 var v = this.get(i);
144 n = (n << 7) | (v & 0x7F);
145 bits += 7;
146 if (!(v & 0x80)) { // finished
147 if (s == undefined)
148 s = parseInt(n / 40) + "." + (n % 40);
149 else
150 s += "." + ((bits >= 31) ? "bigint" : n);
151 n = bits = 0;
152 }
153 s += String.fromCharCode();
154 }
155 return s;
156}
157
158function ASN1(stream, header, length, tag, sub) {
159 this.stream = stream;
160 this.header = header;
161 this.length = length;
162 this.tag = tag;
163 this.sub = sub;
164}
165ASN1.prototype.typeName = function() {
166 if (this.tag == undefined)
167 return "unknown";
168 var tagClass = this.tag >> 6;
169 var tagConstructed = (this.tag >> 5) & 1;
170 var tagNumber = this.tag & 0x1F;
171 switch (tagClass) {
172 case 0: // universal
173 switch (tagNumber) {
174 case 0x00: return "EOC";
175 case 0x01: return "BOOLEAN";
176 case 0x02: return "INTEGER";
177 case 0x03: return "BIT_STRING";
178 case 0x04: return "OCTET_STRING";
179 case 0x05: return "NULL";
180 case 0x06: return "OBJECT_IDENTIFIER";
181 case 0x07: return "ObjectDescriptor";
182 case 0x08: return "EXTERNAL";
183 case 0x09: return "REAL";
184 case 0x0A: return "ENUMERATED";
185 case 0x0B: return "EMBEDDED_PDV";
186 case 0x0C: return "UTF8String";
187 case 0x10: return "SEQUENCE";
188 case 0x11: return "SET";
189 case 0x12: return "NumericString";
190 case 0x13: return "PrintableString"; // ASCII subset
191 case 0x14: return "TeletexString"; // aka T61String
192 case 0x15: return "VideotexString";
193 case 0x16: return "IA5String"; // ASCII
194 case 0x17: return "UTCTime";
195 case 0x18: return "GeneralizedTime";
196 case 0x19: return "GraphicString";
197 case 0x1A: return "VisibleString"; // ASCII subset
198 case 0x1B: return "GeneralString";
199 case 0x1C: return "UniversalString";
200 case 0x1E: return "BMPString";
201 default: return "Universal_" + tagNumber.toString(16);
202 }
203 case 1: return "Application_" + tagNumber.toString(16);
204 case 2: return "[" + tagNumber + "]"; // Context
205 case 3: return "Private_" + tagNumber.toString(16);
206 }
207}
208ASN1.prototype.content = function() {
209 if (this.tag == undefined)
210 return null;
211 var tagClass = this.tag >> 6;
212 if (tagClass != 0) // universal
213 return (this.sub == null) ? null : "(" + this.sub.length + ")";
214 var tagNumber = this.tag & 0x1F;
215 var content = this.posContent();
216 var len = Math.abs(this.length);
217 switch (tagNumber) {
218 case 0x01: // BOOLEAN
219 return (this.stream.get(content) == 0) ? "false" : "true";
220 case 0x02: // INTEGER
221 return this.stream.parseInteger(content, content + len);
222 case 0x03: // BIT_STRING
223 return this.sub ? "(" + this.sub.length + " elem)" :
224 this.stream.parseBitString(content, content + len)
225 case 0x04: // OCTET_STRING
226 return this.sub ? "(" + this.sub.length + " elem)" :
227 this.stream.parseOctetString(content, content + len)
228 //case 0x05: // NULL
229 case 0x06: // OBJECT_IDENTIFIER
230 return this.stream.parseOID(content, content + len);
231 //case 0x07: // ObjectDescriptor
232 //case 0x08: // EXTERNAL
233 //case 0x09: // REAL
234 //case 0x0A: // ENUMERATED
235 //case 0x0B: // EMBEDDED_PDV
236 case 0x10: // SEQUENCE
237 case 0x11: // SET
238 return "(" + this.sub.length + " elem)";
239 case 0x0C: // UTF8String
240 return this.stream.parseStringUTF(content, content + len);
241 case 0x12: // NumericString
242 case 0x13: // PrintableString
243 case 0x14: // TeletexString
244 case 0x15: // VideotexString
245 case 0x16: // IA5String
246 //case 0x19: // GraphicString
247 case 0x1A: // VisibleString
248 //case 0x1B: // GeneralString
249 //case 0x1C: // UniversalString
250 //case 0x1E: // BMPString
251 return this.stream.parseStringISO(content, content + len);
252 case 0x17: // UTCTime
253 case 0x18: // GeneralizedTime
254 return this.stream.parseTime(content, content + len);
255 }
256 return null;
257}
258ASN1.prototype.toString = function() {
259 return this.typeName() + "@" + this.stream.pos + "[header:" + this.header + ",length:" + this.length + ",sub:" + ((this.sub == null) ? 'null' : this.sub.length) + "]";
260}
261ASN1.prototype.print = function(indent) {
262 if (indent == undefined) indent = '';
263 document.writeln(indent + this);
264 if (this.sub != null) {
265 indent += ' ';
266 for (var i = 0, max = this.sub.length; i < max; ++i)
267 this.sub[i].print(indent);
268 }
269}
270ASN1.prototype.toPrettyString = function(indent) {
271 if (indent == undefined) indent = '';
272 var s = indent + this.typeName() + " @" + this.stream.pos;
273 if (this.length >= 0)
274 s += "+";
275 s += this.length;
276 if (this.tag & 0x20)
277 s += " (constructed)";
278 else if (((this.tag == 0x03) || (this.tag == 0x04)) && (this.sub != null))
279 s += " (encapsulates)";
280 s += "\n";
281 if (this.sub != null) {
282 indent += ' ';
283 for (var i = 0, max = this.sub.length; i < max; ++i)
284 s += this.sub[i].toPrettyString(indent);
285 }
286 return s;
287}
288ASN1.prototype.toDOM = function() {
289 var node = document.createElement("div");
290 node.className = "node";
291 node.asn1 = this;
292 var head = document.createElement("div");
293 head.className = "head";
294 var s = this.typeName().replace(/_/g, " ");
295 head.innerHTML = s;
296 var content = this.content();
297 if (content != null) {
298 content = String(content).replace(/</g, "&lt;");
299 var preview = document.createElement("span");
300 preview.className = "preview";
301 preview.innerHTML = content;
302 head.appendChild(preview);
303 }
304 node.appendChild(head);
305 this.node = node;
306 this.head = head;
307 var value = document.createElement("div");
308 value.className = "value";
309 s = "Offset: " + this.stream.pos + "<br/>";
310 s += "Length: " + this.header + "+";
311 if (this.length >= 0)
312 s += this.length;
313 else
314 s += (-this.length) + " (undefined)";
315 if (this.tag & 0x20)
316 s += "<br/>(constructed)";
317 else if (((this.tag == 0x03) || (this.tag == 0x04)) && (this.sub != null))
318 s += "<br/>(encapsulates)";
319 //TODO if (this.tag == 0x03) s += "Unused bits: "
320 if (content != null) {
321 s += "<br/>Value:<br/><b>" + content + "</b>";
322 if ((typeof(oids) == 'object') && (this.tag == 0x06)) {
323 var oid = oids[content];
324 if (oid) {
325 if (oid.d) s += "<br/>" + oid.d;
326 if (oid.c) s += "<br/>" + oid.c;
327 if (oid.w) s += "<br/>(warning!)";
328 }
329 }
330 }
331 value.innerHTML = s;
332 node.appendChild(value);
333 var sub = document.createElement("div");
334 sub.className = "sub";
335 if (this.sub != null) {
336 for (var i = 0, max = this.sub.length; i < max; ++i)
337 sub.appendChild(this.sub[i].toDOM());
338 }
339 node.appendChild(sub);
340 head.switchNode = node;
341 head.onclick = function() {
342 var node = this.switchNode;
343 node.className = (node.className == "node collapsed") ? "node" : "node collapsed";
344 };
345 return node;
346}
347ASN1.prototype.posStart = function() {
348 return this.stream.pos;
349}
350ASN1.prototype.posContent = function() {
351 return this.stream.pos + this.header;
352}
353ASN1.prototype.posEnd = function() {
354 return this.stream.pos + this.header + Math.abs(this.length);
355}
356ASN1.prototype.fakeHover = function(current) {
357 this.node.className += " hover";
358 if (current)
359 this.head.className += " hover";
360}
361ASN1.prototype.fakeOut = function(current) {
362 var re = / ?hover/;
363 this.node.className = this.node.className.replace(re, "");
364 if (current)
365 this.head.className = this.head.className.replace(re, "");
366}
367ASN1.prototype.toHexDOM_sub = function(node, className, stream, start, end) {
368 if (start >= end)
369 return;
370 var sub = document.createElement("span");
371 sub.className = className;
372 sub.appendChild(document.createTextNode(
373 stream.hexDump(start, end)));
374 node.appendChild(sub);
375}
376ASN1.prototype.toHexDOM = function(root) {
377 var node = document.createElement("span");
378 node.className = 'hex';
379 if (root == undefined) root = node;
380 this.head.hexNode = node;
381 this.head.onmouseover = function() { this.hexNode.className = "hexCurrent"; }
382 this.head.onmouseout = function() { this.hexNode.className = "hex"; }
383 node.asn1 = this;
384 node.onmouseover = function() {
385 var current = !root.selected;
386 if (current) {
387 root.selected = this.asn1;
388 this.className = "hexCurrent";
389 }
390 this.asn1.fakeHover(current);
391 }
392 node.onmouseout = function() {
393 var current = (root.selected == this.asn1);
394 this.asn1.fakeOut(current);
395 if (current) {
396 root.selected = null;
397 this.className = "hex";
398 }
399 }
400 this.toHexDOM_sub(node, "tag", this.stream, this.posStart(), this.posStart() + 1);
401 this.toHexDOM_sub(node, (this.length >= 0) ? "dlen" : "ulen", this.stream, this.posStart() + 1, this.posContent());
402 if (this.sub == null)
403 node.appendChild(document.createTextNode(
404 this.stream.hexDump(this.posContent(), this.posEnd())));
405 else if (this.sub.length > 0) {
406 var first = this.sub[0];
407 var last = this.sub[this.sub.length - 1];
408 this.toHexDOM_sub(node, "intro", this.stream, this.posContent(), first.posStart());
409 for (var i = 0, max = this.sub.length; i < max; ++i)
410 node.appendChild(this.sub[i].toHexDOM(root));
411 this.toHexDOM_sub(node, "outro", this.stream, last.posEnd(), this.posEnd());
412 }
413 return node;
414}
415ASN1.decodeLength = function(stream) {
416 var buf = stream.get();
417 var len = buf & 0x7F;
418 if (len == buf)
419 return len;
420 if (len > 3)
421 throw "Length over 24 bits not supported at position " + (stream.pos - 1);
422 if (len == 0)
423 return -1; // undefined
424 buf = 0;
425 for (var i = 0; i < len; ++i)
426 buf = (buf << 8) | stream.get();
427 return buf;
428}
429ASN1.hasContent = function(tag, len, stream) {
430 if (tag & 0x20) // constructed
431 return true;
432 if ((tag < 0x03) || (tag > 0x04))
433 return false;
434 var p = new Stream(stream);
435 if (tag == 0x03) p.get(); // BitString unused bits, must be in [0, 7]
436 var subTag = p.get();
437 if ((subTag >> 6) & 0x01) // not (universal or context)
438 return false;
439 try {
440 var subLength = ASN1.decodeLength(p);
441 return ((p.pos - stream.pos) + subLength == len);
442 } catch (exception) {
443 return false;
444 }
445}
446ASN1.decode = function(stream) {
447 if (!(stream instanceof Stream))
448 stream = new Stream(stream, 0);
449 var streamStart = new Stream(stream);
450 var tag = stream.get();
451 var len = ASN1.decodeLength(stream);
452 var header = stream.pos - streamStart.pos;
453 var sub = null;
454 if (ASN1.hasContent(tag, len, stream)) {
455 // it has content, so we decode it
456 var start = stream.pos;
457 if (tag == 0x03) stream.get(); // skip BitString unused bits, must be in [0, 7]
458 sub = [];
459 if (len >= 0) {
460 // definite length
461 var end = start + len;
462 while (stream.pos < end)
463 sub[sub.length] = ASN1.decode(stream);
464 if (stream.pos != end)
465 throw "Content size is not correct for container starting at offset " + start;
466 } else {
467 // undefined length
468 try {
469 for (;;) {
470 var s = ASN1.decode(stream);
471 if (s.tag == 0)
472 break;
473 sub[sub.length] = s;
474 }
475 len = start - stream.pos;
476 } catch (e) {
477 throw "Exception while decoding undefined length content: " + e;
478 }
479 }
480 } else
481 stream.pos += len; // skip content
482 return new ASN1(streamStart, header, len, tag, sub);
483}
484/*
485ASN1.test = function() {
486 var test = [
487 { value: [0x27], expected: 0x27 },
488 { value: [0x81, 0xC9], expected: 0xC9 },
489 { value: [0x83, 0xFE, 0xDC, 0xBA], expected: 0xFEDCBA },
490 ];
491 for (var i = 0, max = test.length; i < max; ++i) {
492 var pos = 0;
493 var stream = new Stream(test[i].value, 0);
494 var res = ASN1.decodeLength(stream);
495 if (res != test[i].expected)
496 document.write("In test[" + i + "] expected " + test[i].expected + " got " + res + "\n");
497 }
498}*/