forked from lapo-luchini/asn1js
-
Notifications
You must be signed in to change notification settings - Fork 0
/
asn1.js
445 lines (437 loc) · 15.4 KB
/
asn1.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
// ASN.1 JavaScript decoder
// Copyright (c) 2008-2014 Lapo Luchini <[email protected]>
// 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",
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)?)?$/,
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)?)?$/;
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 {
// enc should be an array or a binary string
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 (typeof this.enc == "string") ? this.enc.charCodeAt(pos) : 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 = (shortYear ? reTimeS : 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] += (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 = "";
for (var i = start + 1; i < end; ++i) {
var b = this.get(i),
skip = (i == end - 1) ? unusedBit : 0;
for (var j = 7; j >= skip; --j)
s += (b >> j) & 1 ? "1" : "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))) {
// 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.";
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; }
})();