diff --git a/.gitignore b/.gitignore index eb03e3e..8fb7c91 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,42 @@ -node_modules +# Logs +logs *.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release +dist + +# Dependency directories +node_modules +jspm_packages +typings + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +.nyc_output +coverage diff --git a/lib/ber/errors.js b/lib/ber/errors.js deleted file mode 100644 index ff21d4f..0000000 --- a/lib/ber/errors.js +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2011 Mark Cavage All rights reserved. - - -module.exports = { - - newInvalidAsn1Error: function(msg) { - var e = new Error(); - e.name = 'InvalidAsn1Error'; - e.message = msg || ''; - return e; - } - -}; diff --git a/lib/ber/index.d.ts b/lib/ber/index.d.ts new file mode 100644 index 0000000..089e2fe --- /dev/null +++ b/lib/ber/index.d.ts @@ -0,0 +1,2 @@ +export { Reader } from './reader'; +export { Writer } from './writer'; diff --git a/lib/ber/index.js b/lib/ber/index.js index 4fb90ae..52d637b 100644 --- a/lib/ber/index.js +++ b/lib/ber/index.js @@ -1,27 +1,6 @@ // Copyright 2011 Mark Cavage All rights reserved. - -var errors = require('./errors'); -var types = require('./types'); - -var Reader = require('./reader'); -var Writer = require('./writer'); - - -///--- Exports - -module.exports = { - - Reader: Reader, - - Writer: Writer - -}; - -for (var t in types) { - if (types.hasOwnProperty(t)) - module.exports[t] = types[t]; -} -for (var e in errors) { - if (errors.hasOwnProperty(e)) - module.exports[e] = errors[e]; -} +"use strict"; +var reader_1 = require('./reader'); +exports.Reader = reader_1.Reader; +var writer_1 = require('./writer'); +exports.Writer = writer_1.Writer; diff --git a/lib/ber/reader.d.ts b/lib/ber/reader.d.ts new file mode 100644 index 0000000..d704660 --- /dev/null +++ b/lib/ber/reader.d.ts @@ -0,0 +1,47 @@ +import { ASN1 } from '../common'; +export declare class Reader { + private _buf; + private _size; + private _len; + private _offset; + constructor(data: Buffer); + readonly length: number; + readonly offset: number; + readonly remain: number; + readonly buffer: Buffer; + /** + * Reads a single byte and advances offset; you can pass in `true` to make this + * a "peek" operation (i.e., get the byte, but don't advance the offset). + * + * @param {Boolean} peek true means don't move offset. + * @return {Number} the next byte, null if not enough data. + */ + readByte(peek: boolean): number; + peek(): number; + /** + * Reads a (potentially) variable length off the BER buffer. This call is + * not really meant to be called directly, as callers have to manipulate + * the internal buffer afterwards. + * + * As a result of this call, you can call `Reader.length`, until the + * next thing called that does a readLength. + * + * @return {Number} the amount of offset to advance the buffer. + * @throws {InvalidAsn1Error} on bad ASN.1 + */ + readLength(offset: any): any; + /** + * Parses the next sequence in this BER buffer. + * + * To get the length of the sequence, call `Reader.length`. + * + * @return {Number} the sequence's tag. + */ + readSequence(tag: number): number; + readInt(): number; + readBoolean(): boolean; + readEnumeration(): number; + readString(tag?: ASN1, retbuf?: Boolean): Buffer | string; + readOID: (tag?: ASN1) => string; + private _readTag(tag); +} diff --git a/lib/ber/reader.js b/lib/ber/reader.js index 0a00e98..d475e01 100644 --- a/lib/ber/reader.js +++ b/lib/ber/reader.js @@ -1,261 +1,183 @@ // Copyright 2011 Mark Cavage All rights reserved. - -var assert = require('assert'); - -var ASN1 = require('./types'); -var errors = require('./errors'); - - -///--- Globals - -var newInvalidAsn1Error = errors.newInvalidAsn1Error; - - - -///--- API - -function Reader(data) { - if (!data || !Buffer.isBuffer(data)) - throw new TypeError('data must be a node Buffer'); - - this._buf = data; - this._size = data.length; - - // These hold the "current" state - this._len = 0; - this._offset = 0; -} - -Object.defineProperty(Reader.prototype, 'length', { - enumerable: true, - get: function () { return (this._len); } -}); - -Object.defineProperty(Reader.prototype, 'offset', { - enumerable: true, - get: function () { return (this._offset); } -}); - -Object.defineProperty(Reader.prototype, 'remain', { - get: function () { return (this._size - this._offset); } -}); - -Object.defineProperty(Reader.prototype, 'buffer', { - get: function () { return (this._buf.slice(this._offset)); } -}); - - -/** - * Reads a single byte and advances offset; you can pass in `true` to make this - * a "peek" operation (i.e., get the byte, but don't advance the offset). - * - * @param {Boolean} peek true means don't move offset. - * @return {Number} the next byte, null if not enough data. - */ -Reader.prototype.readByte = function(peek) { - if (this._size - this._offset < 1) - return null; - - var b = this._buf[this._offset] & 0xff; - - if (!peek) - this._offset += 1; - - return b; -}; - - -Reader.prototype.peek = function() { - return this.readByte(true); -}; - - -/** - * Reads a (potentially) variable length off the BER buffer. This call is - * not really meant to be called directly, as callers have to manipulate - * the internal buffer afterwards. - * - * As a result of this call, you can call `Reader.length`, until the - * next thing called that does a readLength. - * - * @return {Number} the amount of offset to advance the buffer. - * @throws {InvalidAsn1Error} on bad ASN.1 - */ -Reader.prototype.readLength = function(offset) { - if (offset === undefined) - offset = this._offset; - - if (offset >= this._size) - return null; - - var lenB = this._buf[offset++] & 0xff; - if (lenB === null) - return null; - - if ((lenB & 0x80) == 0x80) { - lenB &= 0x7f; - - if (lenB == 0) - throw newInvalidAsn1Error('Indefinite length not supported'); - - if (lenB > 4) - throw newInvalidAsn1Error('encoding too long'); - - if (this._size - offset < lenB) - return null; - - this._len = 0; - for (var i = 0; i < lenB; i++) - this._len = (this._len << 8) + (this._buf[offset++] & 0xff); - - } else { - // Wasn't a variable length - this._len = lenB; - } - - return offset; -}; - - -/** - * Parses the next sequence in this BER buffer. - * - * To get the length of the sequence, call `Reader.length`. - * - * @return {Number} the sequence's tag. - */ -Reader.prototype.readSequence = function(tag) { - var seq = this.peek(); - if (seq === null) - return null; - if (tag !== undefined && tag !== seq) - throw newInvalidAsn1Error('Expected 0x' + tag.toString(16) + - ': got 0x' + seq.toString(16)); - - var o = this.readLength(this._offset + 1); // stored in `length` - if (o === null) - return null; - - this._offset = o; - return seq; -}; - - -Reader.prototype.readInt = function() { - return this._readTag(ASN1.Integer); -}; - - -Reader.prototype.readBoolean = function() { - return (this._readTag(ASN1.Boolean) === 0 ? false : true); -}; - - -Reader.prototype.readEnumeration = function() { - return this._readTag(ASN1.Enumeration); -}; - - -Reader.prototype.readString = function(tag, retbuf) { - if (!tag) - tag = ASN1.OctetString; - - var b = this.peek(); - if (b === null) - return null; - - if (b !== tag) - throw newInvalidAsn1Error('Expected 0x' + tag.toString(16) + - ': got 0x' + b.toString(16)); - - var o = this.readLength(this._offset + 1); // stored in `length` - - if (o === null) - return null; - - if (this.length > this._size - o) - return null; - - this._offset = o; - - if (this.length === 0) - return retbuf ? new Buffer(0) : ''; - - var str = this._buf.slice(this._offset, this._offset + this.length); - this._offset += this.length; - - return retbuf ? str : str.toString('utf8'); -}; - -Reader.prototype.readOID = function(tag) { - if (!tag) - tag = ASN1.OID; - - var b = this.readString(tag, true); - if (b === null) - return null; - - var values = []; - var value = 0; - - for (var i = 0; i < b.length; i++) { - var byte = b[i] & 0xff; - - value <<= 7; - value += byte & 0x7f; - if ((byte & 0x80) == 0) { - values.push(value); - value = 0; +"use strict"; +const common_1 = require('../common'); +class Reader { + constructor(data) { + // These hold the "current" state + this._len = 0; + this._offset = 0; + this.readOID = function (tag) { + if (!tag) + tag = common_1.ASN1.OID; + let b = this.readString(tag, true); + if (b === null) + return null; + let values = []; + let value = 0; + for (let i = 0; i < b.length; i++) { + let byte = b[i] & 0xff; + value <<= 7; + value += byte & 0x7f; + if ((byte & 0x80) == 0) { + values.push(value); + value = 0; + } + } + value = values.shift(); + values.unshift(value % 40); + values.unshift((value / 40) >> 0); + return values.join('.'); + }; + if (!data || !Buffer.isBuffer(data)) + throw new TypeError('data must be a node Buffer'); + this._buf = data; + this._size = data.length; + } + get length() { + return this._len; + } + get offset() { + return this._offset; + } + get remain() { + return this._size - this._offset; + } + get buffer() { + return this._buf.slice(this._offset); + } + /** + * Reads a single byte and advances offset; you can pass in `true` to make this + * a "peek" operation (i.e., get the byte, but don't advance the offset). + * + * @param {Boolean} peek true means don't move offset. + * @return {Number} the next byte, null if not enough data. + */ + readByte(peek) { + if (this._size - this._offset < 1) + return null; + let b = this._buf[this._offset] & 0xff; + if (!peek) + this._offset += 1; + return b; + } + peek() { + return this.readByte(true); + } + /** + * Reads a (potentially) variable length off the BER buffer. This call is + * not really meant to be called directly, as callers have to manipulate + * the internal buffer afterwards. + * + * As a result of this call, you can call `Reader.length`, until the + * next thing called that does a readLength. + * + * @return {Number} the amount of offset to advance the buffer. + * @throws {InvalidAsn1Error} on bad ASN.1 + */ + readLength(offset) { + if (offset === undefined) + offset = this._offset; + if (offset >= this._size) + return null; + let lenB = this._buf[offset++] & 0xff; + if (lenB === null) + return null; + if ((lenB & 0x80) === 0x80) { + lenB &= 0x7f; + if (lenB == 0) + throw new common_1.InvalidAsn1Error('Indefinite length not supported'); + if (lenB > 4) + throw new common_1.InvalidAsn1Error('encoding too long'); + if (this._size - offset < lenB) + return null; + this._len = 0; + for (let i = 0; i < lenB; i++) + this._len = (this._len << 8) + (this._buf[offset++] & 0xff); + } + else { + // Wasn't a variable length + this._len = lenB; + } + return offset; } - } - - value = values.shift(); - values.unshift(value % 40); - values.unshift((value / 40) >> 0); - - return values.join('.'); -}; - - -Reader.prototype._readTag = function(tag) { - assert.ok(tag !== undefined); - - var b = this.peek(); - - if (b === null) - return null; - - if (b !== tag) - throw newInvalidAsn1Error('Expected 0x' + tag.toString(16) + - ': got 0x' + b.toString(16)); - - var o = this.readLength(this._offset + 1); // stored in `length` - if (o === null) - return null; - - if (this.length > 4) - throw newInvalidAsn1Error('Integer too long: ' + this.length); - - if (this.length > this._size - o) - return null; - this._offset = o; - - var fb = this._buf[this._offset]; - var value = 0; - - for (var i = 0; i < this.length; i++) { - value <<= 8; - value |= (this._buf[this._offset++] & 0xff); - } - - if ((fb & 0x80) == 0x80 && i !== 4) - value -= (1 << (i * 8)); - - return value >> 0; -}; - - - -///--- Exported API - -module.exports = Reader; + ; + /** + * Parses the next sequence in this BER buffer. + * + * To get the length of the sequence, call `Reader.length`. + * + * @return {Number} the sequence's tag. + */ + readSequence(tag) { + let seq = this.peek(); + if (seq === null) + return null; + if (tag !== undefined && tag !== seq) + throw new common_1.InvalidAsn1Error('Expected 0x' + tag.toString(16) + ': got 0x' + seq.toString(16)); + let o = this.readLength(this._offset + 1); // stored in `length` + if (o === null) + return null; + this._offset = o; + return seq; + } + ; + readInt() { + return this._readTag(common_1.ASN1.Integer); + } + ; + readBoolean() { + return this._readTag(common_1.ASN1.Boolean) === 0 ? false : true; + } + ; + readEnumeration() { + return this._readTag(common_1.ASN1.Enumeration); + } + ; + readString(tag, retbuf = false) { + if (!tag) + tag = common_1.ASN1.OctetString; + let b = this.peek(); + if (b === null) + return null; + if (b !== tag) + throw new common_1.InvalidAsn1Error('Expected 0x' + tag.toString(16) + ': got 0x' + b.toString(16)); + let o = this.readLength(this._offset + 1); // stored in `length` + if (o === null) + return null; + if (this.length > this._size - o) + return null; + this._offset = o; + if (this.length === 0) + return retbuf ? new Buffer(0) : ''; + let str = this._buf.slice(this._offset, this._offset + this.length); + this._offset += this.length; + return retbuf ? str : str.toString('utf8'); + } + ; + _readTag(tag) { + let b = this.peek(); + if (b === null) + return null; + if (b !== tag) + throw new common_1.InvalidAsn1Error('Expected 0x' + tag.toString(16) + ': got 0x' + b.toString(16)); + let o = this.readLength(this._offset + 1); // stored in `length` + if (o === null) + return null; + if (this.length > 4) + throw new common_1.InvalidAsn1Error('Integer too long: ' + this.length); + if (this.length > this._size - o) + return null; + this._offset = o; + let fb = this._buf[this._offset]; + let value = 0; + let i = 0; + for (i = 0; i < this.length; i++) { + value <<= 8; + value |= (this._buf[this._offset++] & 0xff); + } + if ((fb & 0x80) === 0x80 && i !== 4) + value -= (1 << (i * 8)); + return value >> 0; + } +} +exports.Reader = Reader; diff --git a/lib/ber/types.js b/lib/ber/types.js deleted file mode 100644 index 8aea000..0000000 --- a/lib/ber/types.js +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2011 Mark Cavage All rights reserved. - - -module.exports = { - EOC: 0, - Boolean: 1, - Integer: 2, - BitString: 3, - OctetString: 4, - Null: 5, - OID: 6, - ObjectDescriptor: 7, - External: 8, - Real: 9, // float - Enumeration: 10, - PDV: 11, - Utf8String: 12, - RelativeOID: 13, - Sequence: 16, - Set: 17, - NumericString: 18, - PrintableString: 19, - T61String: 20, - VideotexString: 21, - IA5String: 22, - UTCTime: 23, - GeneralizedTime: 24, - GraphicString: 25, - VisibleString: 26, - GeneralString: 28, - UniversalString: 29, - CharacterString: 30, - BMPString: 31, - Constructor: 32, - Context: 128 -}; diff --git a/lib/ber/writer.d.ts b/lib/ber/writer.d.ts new file mode 100644 index 0000000..a8500c7 --- /dev/null +++ b/lib/ber/writer.d.ts @@ -0,0 +1,26 @@ +import { ASN1 } from '../common'; +export declare class Writer { + private _buf; + private _size; + private _offset; + private _options; + private _seq; + constructor(options: any); + readonly buffer: Buffer; + writeLength(len: number): void; + writeByte(b: number): void; + writeInt(i: number, tag?: ASN1): void; + writeNull(): void; + writeEnumeration(i: number, tag?: ASN1): void; + writeBoolean(b: boolean, tag?: ASN1): void; + writeString(s: string, tag?: ASN1): void; + writeBuffer(buf: Buffer, tag?: ASN1): void; + writeStringArray(strings: string[]): void; + writeOID(s: string, tag?: ASN1): void; + startSequence(tag?: ASN1): void; + endSequence(): void; + private _merge(fromObject, toObject); + private _shift(start, len, shift); + private _ensure(len); + private _encodeOctet(bytes, octet); +} diff --git a/lib/ber/writer.js b/lib/ber/writer.js index d9d99af..de32ead 100644 --- a/lib/ber/writer.js +++ b/lib/ber/writer.js @@ -1,316 +1,267 @@ // Copyright 2011 Mark Cavage All rights reserved. - -var assert = require('assert'); -var ASN1 = require('./types'); -var errors = require('./errors'); - - -///--- Globals - -var newInvalidAsn1Error = errors.newInvalidAsn1Error; - -var DEFAULT_OPTS = { - size: 1024, - growthFactor: 8 -}; - - -///--- Helpers - -function merge(from, to) { - assert.ok(from); - assert.equal(typeof(from), 'object'); - assert.ok(to); - assert.equal(typeof(to), 'object'); - - var keys = Object.getOwnPropertyNames(from); - keys.forEach(function(key) { - if (to[key]) - return; - - var value = Object.getOwnPropertyDescriptor(from, key); - Object.defineProperty(to, key, value); - }); - - return to; -} - - - -///--- API - -function Writer(options) { - options = merge(DEFAULT_OPTS, options || {}); - - this._buf = new Buffer(options.size || 1024); - this._size = this._buf.length; - this._offset = 0; - this._options = options; - - // A list of offsets in the buffer where we need to insert - // sequence tag/len pairs. - this._seq = []; +"use strict"; +const common_1 = require('../common'); +const assert = require('assert'); +class Options { } - -Object.defineProperty(Writer.prototype, 'buffer', { - get: function () { - if (this._seq.length) - throw new InvalidAsn1Error(this._seq.length + ' unended sequence(s)'); - - return (this._buf.slice(0, this._offset)); - } -}); - -Writer.prototype.writeByte = function(b) { - if (typeof(b) !== 'number') - throw new TypeError('argument must be a Number'); - - this._ensure(1); - this._buf[this._offset++] = b; -}; - - -Writer.prototype.writeInt = function(i, tag) { - if (typeof(i) !== 'number') - throw new TypeError('argument must be a Number'); - if (typeof(tag) !== 'number') - tag = ASN1.Integer; - - var sz = 4; - - while ((((i & 0xff800000) === 0) || ((i & 0xff800000) === 0xff800000 >> 0)) && - (sz > 1)) { - sz--; - i <<= 8; - } - - if (sz > 4) - throw new InvalidAsn1Error('BER ints cannot be > 0xffffffff'); - - this._ensure(2 + sz); - this._buf[this._offset++] = tag; - this._buf[this._offset++] = sz; - - while (sz-- > 0) { - this._buf[this._offset++] = ((i & 0xff000000) >>> 24); - i <<= 8; - } - -}; - - -Writer.prototype.writeNull = function() { - this.writeByte(ASN1.Null); - this.writeByte(0x00); -}; - - -Writer.prototype.writeEnumeration = function(i, tag) { - if (typeof(i) !== 'number') - throw new TypeError('argument must be a Number'); - if (typeof(tag) !== 'number') - tag = ASN1.Enumeration; - - return this.writeInt(i, tag); -}; - - -Writer.prototype.writeBoolean = function(b, tag) { - if (typeof(b) !== 'boolean') - throw new TypeError('argument must be a Boolean'); - if (typeof(tag) !== 'number') - tag = ASN1.Boolean; - - this._ensure(3); - this._buf[this._offset++] = tag; - this._buf[this._offset++] = 0x01; - this._buf[this._offset++] = b ? 0xff : 0x00; -}; - - -Writer.prototype.writeString = function(s, tag) { - if (typeof(s) !== 'string') - throw new TypeError('argument must be a string (was: ' + typeof(s) + ')'); - if (typeof(tag) !== 'number') - tag = ASN1.OctetString; - - var len = Buffer.byteLength(s); - this.writeByte(tag); - this.writeLength(len); - if (len) { - this._ensure(len); - this._buf.write(s, this._offset); - this._offset += len; - } -}; - - -Writer.prototype.writeBuffer = function(buf, tag) { - if (typeof(tag) !== 'number') - throw new TypeError('tag must be a number'); - if (!Buffer.isBuffer(buf)) - throw new TypeError('argument must be a buffer'); - - this.writeByte(tag); - this.writeLength(buf.length); - this._ensure(buf.length); - buf.copy(this._buf, this._offset, 0, buf.length); - this._offset += buf.length; -}; - - -Writer.prototype.writeStringArray = function(strings) { - if ((!strings instanceof Array)) - throw new TypeError('argument must be an Array[String]'); - - var self = this; - strings.forEach(function(s) { - self.writeString(s); - }); +let DEFAULT_OPTS = { + size: 1024, + growthFactor: 8 }; - -// This is really to solve DER cases, but whatever for now -Writer.prototype.writeOID = function(s, tag) { - if (typeof(s) !== 'string') - throw new TypeError('argument must be a string'); - if (typeof(tag) !== 'number') - tag = ASN1.OID; - - if (!/^([0-9]+\.){3,}[0-9]+$/.test(s)) - throw new Error('argument is not a valid OID string'); - - function encodeOctet(bytes, octet) { - if (octet < 128) { - bytes.push(octet); - } else if (octet < 16384) { - bytes.push((octet >>> 7) | 0x80); - bytes.push(octet & 0x7F); - } else if (octet < 2097152) { - bytes.push((octet >>> 14) | 0x80); - bytes.push(((octet >>> 7) | 0x80) & 0xFF); - bytes.push(octet & 0x7F); - } else if (octet < 268435456) { - bytes.push((octet >>> 21) | 0x80); - bytes.push(((octet >>> 14) | 0x80) & 0xFF); - bytes.push(((octet >>> 7) | 0x80) & 0xFF); - bytes.push(octet & 0x7F); - } else { - bytes.push(((octet >>> 28) | 0x80) & 0xFF); - bytes.push(((octet >>> 21) | 0x80) & 0xFF); - bytes.push(((octet >>> 14) | 0x80) & 0xFF); - bytes.push(((octet >>> 7) | 0x80) & 0xFF); - bytes.push(octet & 0x7F); +class Writer { + constructor(options) { + this._offset = 0; + this._options = 0; + // A list of offsets in the buffer where we need to insert sequence tag/len pairs. + this._seq = []; + options = this._merge(DEFAULT_OPTS, options || {}); + this._buf = new Buffer(options.size || 1024); + this._size = this._buf.length; + this._offset = 0; + this._options = options; } - } - - var tmp = s.split('.'); - var bytes = []; - bytes.push(parseInt(tmp[0], 10) * 40 + parseInt(tmp[1], 10)); - tmp.slice(2).forEach(function(b) { - encodeOctet(bytes, parseInt(b, 10)); - }); - - var self = this; - this._ensure(2 + bytes.length); - this.writeByte(tag); - this.writeLength(bytes.length); - bytes.forEach(function(b) { - self.writeByte(b); - }); -}; - - -Writer.prototype.writeLength = function(len) { - if (typeof(len) !== 'number') - throw new TypeError('argument must be a Number'); - - this._ensure(4); - - if (len <= 0x7f) { - this._buf[this._offset++] = len; - } else if (len <= 0xff) { - this._buf[this._offset++] = 0x81; - this._buf[this._offset++] = len; - } else if (len <= 0xffff) { - this._buf[this._offset++] = 0x82; - this._buf[this._offset++] = len >> 8; - this._buf[this._offset++] = len; - } else if (len <= 0xffffff) { - this._buf[this._offset++] = 0x83; - this._buf[this._offset++] = len >> 16; - this._buf[this._offset++] = len >> 8; - this._buf[this._offset++] = len; - } else { - throw new InvalidAsn1ERror('Length too long (> 4 bytes)'); - } -}; - -Writer.prototype.startSequence = function(tag) { - if (typeof(tag) !== 'number') - tag = ASN1.Sequence | ASN1.Constructor; - - this.writeByte(tag); - this._seq.push(this._offset); - this._ensure(3); - this._offset += 3; -}; - - -Writer.prototype.endSequence = function() { - var seq = this._seq.pop(); - var start = seq + 3; - var len = this._offset - start; - - if (len <= 0x7f) { - this._shift(start, len, -2); - this._buf[seq] = len; - } else if (len <= 0xff) { - this._shift(start, len, -1); - this._buf[seq] = 0x81; - this._buf[seq + 1] = len; - } else if (len <= 0xffff) { - this._buf[seq] = 0x82; - this._buf[seq + 1] = len >> 8; - this._buf[seq + 2] = len; - } else if (len <= 0xffffff) { - this._shift(start, len, 1); - this._buf[seq] = 0x83; - this._buf[seq + 1] = len >> 16; - this._buf[seq + 2] = len >> 8; - this._buf[seq + 3] = len; - } else { - throw new InvalidAsn1Error('Sequence too long'); - } -}; - - -Writer.prototype._shift = function(start, len, shift) { - assert.ok(start !== undefined); - assert.ok(len !== undefined); - assert.ok(shift); - - this._buf.copy(this._buf, start + shift, start, start + len); - this._offset += shift; -}; - -Writer.prototype._ensure = function(len) { - assert.ok(len); - - if (this._size - this._offset < len) { - var sz = this._size * this._options.growthFactor; - if (sz - this._offset < len) - sz += len; - - var buf = new Buffer(sz); - - this._buf.copy(buf, 0, 0, this._offset); - this._buf = buf; - this._size = sz; - } -}; - - - -///--- Exported API - -module.exports = Writer; + get buffer() { + if (this._seq.length) + throw new common_1.InvalidAsn1Error(this._seq.length + ' unended sequence(s)'); + return (this._buf.slice(0, this._offset)); + } + writeLength(len) { + if (typeof (len) !== 'number') + throw new TypeError('argument must be a Number'); + this._ensure(4); + if (len <= 0x7f) { + this._buf[this._offset++] = len; + } + else if (len <= 0xff) { + this._buf[this._offset++] = 0x81; + this._buf[this._offset++] = len; + } + else if (len <= 0xffff) { + this._buf[this._offset++] = 0x82; + this._buf[this._offset++] = len >> 8; + this._buf[this._offset++] = len; + } + else if (len <= 0xffffff) { + this._buf[this._offset++] = 0x83; + this._buf[this._offset++] = len >> 16; + this._buf[this._offset++] = len >> 8; + this._buf[this._offset++] = len; + } + else { + throw new common_1.InvalidAsn1Error('Length too long (> 4 bytes)'); + } + } + ; + writeByte(b) { + if (typeof (b) !== 'number') + throw new TypeError('argument must be a Number'); + this._ensure(1); + this._buf[this._offset++] = b; + } + writeInt(i, tag) { + if (typeof (i) !== 'number') + throw new TypeError('argument must be a Number'); + if (typeof (tag) !== 'number') + tag = common_1.ASN1.Integer; + let sz = 4; + while ((((i & 0xff800000) === 0) || ((i & 0xff800000) === 0xff800000 >> 0)) && (sz > 1)) { + sz--; + i <<= 8; + } + if (sz > 4) + throw new common_1.InvalidAsn1Error('BER ints cannot be > 0xffffffff'); + this._ensure(2 + sz); + this._buf[this._offset++] = tag; + this._buf[this._offset++] = sz; + while (sz-- > 0) { + this._buf[this._offset++] = ((i & 0xff000000) >>> 24); + i <<= 8; + } + } + ; + writeNull() { + this.writeByte(common_1.ASN1.Null); + this.writeByte(0x00); + } + ; + writeEnumeration(i, tag) { + if (typeof (i) !== 'number') + throw new TypeError('argument must be a Number'); + if (typeof (tag) !== 'number') + tag = common_1.ASN1.Enumeration; + return this.writeInt(i, tag); + } + ; + writeBoolean(b, tag) { + if (typeof (b) !== 'boolean') + throw new TypeError('argument must be a Boolean'); + if (typeof (tag) !== 'number') + tag = common_1.ASN1.Boolean; + this._ensure(3); + this._buf[this._offset++] = tag; + this._buf[this._offset++] = 0x01; + this._buf[this._offset++] = b ? 0xff : 0x00; + } + ; + writeString(s, tag) { + if (typeof (s) !== 'string') + throw new TypeError('argument must be a string (was: ' + typeof (s) + ')'); + if (typeof (tag) !== 'number') + tag = common_1.ASN1.OctetString; + let len = Buffer.byteLength(s); + this.writeByte(tag); + this.writeLength(len); + if (len) { + this._ensure(len); + this._buf.write(s, this._offset); + this._offset += len; + } + } + ; + writeBuffer(buf, tag) { + if (typeof (tag) !== 'number') + throw new TypeError('tag must be a number'); + if (!Buffer.isBuffer(buf)) + throw new TypeError('argument must be a buffer'); + this.writeByte(tag); + this.writeLength(buf.length); + this._ensure(buf.length); + buf.copy(this._buf, this._offset, 0, buf.length); + this._offset += buf.length; + } + ; + writeStringArray(strings) { + if (!(strings instanceof Array)) + throw new TypeError('argument must be an Array[String]'); + let self = this; + strings.forEach(function (s) { + self.writeString(s); + }); + } + ; + // This is really to solve DER cases, but whatever for now + writeOID(s, tag) { + if (typeof (s) !== 'string') + throw new TypeError('argument must be a string'); + if (typeof (tag) !== 'number') + tag = common_1.ASN1.OID; + if (!/^([0-9]+\.){3,}[0-9]+$/.test(s)) + throw new Error('argument is not a valid OID string'); + let tmp = s.split('.'); + let bytes = []; + bytes.push(parseInt(tmp[0], 10) * 40 + parseInt(tmp[1], 10)); + let self = this; + tmp.slice(2).forEach(function (b) { + self._encodeOctet(bytes, parseInt(b, 10)); + }); + this._ensure(2 + bytes.length); + this.writeByte(tag); + this.writeLength(bytes.length); + bytes.forEach(function (b) { + self.writeByte(b); + }); + } + ; + startSequence(tag) { + if (typeof (tag) !== 'number') + tag = common_1.ASN1.Sequence | common_1.ASN1.Constructor; + this.writeByte(tag); + this._seq.push(this._offset); + this._ensure(3); + this._offset += 3; + } + ; + endSequence() { + let seq = this._seq.pop(); + let start = seq + 3; + let len = this._offset - start; + if (len <= 0x7f) { + this._shift(start, len, -2); + this._buf[seq] = len; + } + else if (len <= 0xff) { + this._shift(start, len, -1); + this._buf[seq] = 0x81; + this._buf[seq + 1] = len; + } + else if (len <= 0xffff) { + this._buf[seq] = 0x82; + this._buf[seq + 1] = len >> 8; + this._buf[seq + 2] = len; + } + else if (len <= 0xffffff) { + this._shift(start, len, 1); + this._buf[seq] = 0x83; + this._buf[seq + 1] = len >> 16; + this._buf[seq + 2] = len >> 8; + this._buf[seq + 3] = len; + } + else { + throw new common_1.InvalidAsn1Error('Sequence too long'); + } + } + ; + _merge(fromObject, toObject) { + assert.ok(fromObject); + assert.equal(typeof (fromObject), 'object'); + assert.ok(toObject); + assert.equal(typeof (toObject), 'object'); + let keys = Object.getOwnPropertyNames(fromObject); + keys.forEach(function (key) { + if (toObject[key]) + return; + let value = Object.getOwnPropertyDescriptor(fromObject, key); + Object.defineProperty(toObject, key, value); + }); + return toObject; + } + _shift(start, len, shift) { + assert.ok(start !== undefined); + assert.ok(len !== undefined); + assert.ok(shift); + this._buf.copy(this._buf, start + shift, start, start + len); + this._offset += shift; + } + ; + _ensure(len) { + assert.ok(len); + if (this._size - this._offset < len) { + let sz = this._size * this._options.growthFactor; + if (sz - this._offset < len) + sz += len; + let buf = new Buffer(sz); + this._buf.copy(buf, 0, 0, this._offset); + this._buf = buf; + this._size = sz; + } + } + ; + _encodeOctet(bytes, octet) { + if (octet < 128) { + bytes.push(octet); + } + else if (octet < 16384) { + bytes.push((octet >>> 7) | 0x80); + bytes.push(octet & 0x7F); + } + else if (octet < 2097152) { + bytes.push((octet >>> 14) | 0x80); + bytes.push(((octet >>> 7) | 0x80) & 0xFF); + bytes.push(octet & 0x7F); + } + else if (octet < 268435456) { + bytes.push((octet >>> 21) | 0x80); + bytes.push(((octet >>> 14) | 0x80) & 0xFF); + bytes.push(((octet >>> 7) | 0x80) & 0xFF); + bytes.push(octet & 0x7F); + } + else { + bytes.push(((octet >>> 28) | 0x80) & 0xFF); + bytes.push(((octet >>> 21) | 0x80) & 0xFF); + bytes.push(((octet >>> 14) | 0x80) & 0xFF); + bytes.push(((octet >>> 7) | 0x80) & 0xFF); + bytes.push(octet & 0x7F); + } + } +} +exports.Writer = Writer; diff --git a/lib/common/errors.d.ts b/lib/common/errors.d.ts new file mode 100644 index 0000000..05aeeb1 --- /dev/null +++ b/lib/common/errors.d.ts @@ -0,0 +1,3 @@ +export declare class InvalidAsn1Error extends Error { + constructor(message: string); +} diff --git a/lib/common/errors.js b/lib/common/errors.js new file mode 100644 index 0000000..dbdeea9 --- /dev/null +++ b/lib/common/errors.js @@ -0,0 +1,10 @@ +// Copyright 2011 Mark Cavage All rights reserved. +"use strict"; +class InvalidAsn1Error extends Error { + constructor(message) { + super(message || ''); + this.name = 'InvalidAsn1Error'; + } +} +exports.InvalidAsn1Error = InvalidAsn1Error; +; diff --git a/lib/common/index.d.ts b/lib/common/index.d.ts new file mode 100644 index 0000000..b7de1f3 --- /dev/null +++ b/lib/common/index.d.ts @@ -0,0 +1,2 @@ +export { ASN1 } from './types'; +export { InvalidAsn1Error } from './errors'; diff --git a/lib/common/index.js b/lib/common/index.js new file mode 100644 index 0000000..16ef34b --- /dev/null +++ b/lib/common/index.js @@ -0,0 +1,6 @@ +// Copyright 2011 Mark Cavage All rights reserved. +"use strict"; +var types_1 = require('./types'); +exports.ASN1 = types_1.ASN1; +var errors_1 = require('./errors'); +exports.InvalidAsn1Error = errors_1.InvalidAsn1Error; diff --git a/lib/common/types.d.ts b/lib/common/types.d.ts new file mode 100644 index 0000000..23604ac --- /dev/null +++ b/lib/common/types.d.ts @@ -0,0 +1,36 @@ +/** + * See http://www.oss.com/asn1/resources/reference/asn1-reference-card.html + */ +export declare enum ASN1 { + EOC = 0, + Boolean = 1, + Integer = 2, + BitString = 3, + OctetString = 4, + Null = 5, + OID = 6, + ObjectDescriptor = 7, + External = 8, + Real = 9, + Enumeration = 10, + PDV = 11, + Utf8String = 12, + RelativeOID = 13, + Sequence = 16, + Set = 17, + NumericString = 18, + PrintableString = 19, + T61String = 20, + VideotexString = 21, + IA5String = 22, + UTCTime = 23, + GeneralizedTime = 24, + GraphicString = 25, + VisibleString = 26, + GeneralString = 28, + UniversalString = 29, + CharacterString = 30, + BMPString = 31, + Constructor = 32, + Context = 128, +} diff --git a/lib/common/types.js b/lib/common/types.js new file mode 100644 index 0000000..7f5bc82 --- /dev/null +++ b/lib/common/types.js @@ -0,0 +1,40 @@ +// Copyright 2011 Mark Cavage All rights reserved. +"use strict"; +/** + * See http://www.oss.com/asn1/resources/reference/asn1-reference-card.html + */ +(function (ASN1) { + ASN1[ASN1["EOC"] = 0] = "EOC"; + ASN1[ASN1["Boolean"] = 1] = "Boolean"; + ASN1[ASN1["Integer"] = 2] = "Integer"; + ASN1[ASN1["BitString"] = 3] = "BitString"; + ASN1[ASN1["OctetString"] = 4] = "OctetString"; + ASN1[ASN1["Null"] = 5] = "Null"; + ASN1[ASN1["OID"] = 6] = "OID"; + ASN1[ASN1["ObjectDescriptor"] = 7] = "ObjectDescriptor"; + ASN1[ASN1["External"] = 8] = "External"; + ASN1[ASN1["Real"] = 9] = "Real"; + ASN1[ASN1["Enumeration"] = 10] = "Enumeration"; + ASN1[ASN1["PDV"] = 11] = "PDV"; + ASN1[ASN1["Utf8String"] = 12] = "Utf8String"; + ASN1[ASN1["RelativeOID"] = 13] = "RelativeOID"; + ASN1[ASN1["Sequence"] = 16] = "Sequence"; + ASN1[ASN1["Set"] = 17] = "Set"; + ASN1[ASN1["NumericString"] = 18] = "NumericString"; + ASN1[ASN1["PrintableString"] = 19] = "PrintableString"; + ASN1[ASN1["T61String"] = 20] = "T61String"; + ASN1[ASN1["VideotexString"] = 21] = "VideotexString"; + ASN1[ASN1["IA5String"] = 22] = "IA5String"; + ASN1[ASN1["UTCTime"] = 23] = "UTCTime"; + ASN1[ASN1["GeneralizedTime"] = 24] = "GeneralizedTime"; + ASN1[ASN1["GraphicString"] = 25] = "GraphicString"; + ASN1[ASN1["VisibleString"] = 26] = "VisibleString"; + ASN1[ASN1["GeneralString"] = 28] = "GeneralString"; + ASN1[ASN1["UniversalString"] = 29] = "UniversalString"; + ASN1[ASN1["CharacterString"] = 30] = "CharacterString"; + ASN1[ASN1["BMPString"] = 31] = "BMPString"; + ASN1[ASN1["Constructor"] = 32] = "Constructor"; + ASN1[ASN1["Context"] = 128] = "Context"; +})(exports.ASN1 || (exports.ASN1 = {})); +var ASN1 = exports.ASN1; +; diff --git a/lib/index.d.ts b/lib/index.d.ts new file mode 100644 index 0000000..3eea598 --- /dev/null +++ b/lib/index.d.ts @@ -0,0 +1,4 @@ +import * as Ber from './ber/index'; +export { ASN1, InvalidAsn1Error } from './common'; +export { Reader as BerReader, Writer as BerWriter } from './ber/index'; +export { Ber as Ber }; diff --git a/lib/index.js b/lib/index.js index d1766e7..368124b 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,20 +1,13 @@ // Copyright 2011 Mark Cavage All rights reserved. - +"use strict"; // If you have no idea what ASN.1 or BER is, see this: // ftp://ftp.rsa.com/pub/pkcs/ascii/layman.asc - -var Ber = require('./ber/index'); - - - +const Ber = require('./ber/index'); +exports.Ber = Ber; ///--- Exported API - -module.exports = { - - Ber: Ber, - - BerReader: Ber.Reader, - - BerWriter: Ber.Writer - -}; +var common_1 = require('./common'); +exports.ASN1 = common_1.ASN1; +exports.InvalidAsn1Error = common_1.InvalidAsn1Error; +var index_1 = require('./ber/index'); +exports.BerReader = index_1.Reader; +exports.BerWriter = index_1.Writer; diff --git a/package.json b/package.json index f6dfb06..717602b 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "contributors": [ "David Gwynne ", "Yunong Xiao ", - "Alex Wilson " + "Alex Wilson ", + "Stef Heyenrath" ], "name": "asn1", "description": "Contains parsers and serializers for ASN.1 (currently BER only)", @@ -13,11 +14,23 @@ "url": "git://github.com/mcavage/node-asn1.git" }, "main": "lib/index.js", + "typings": "lib/index.d.ts", "dependencies": {}, "devDependencies": { - "tap": "0.4.8" + "awesome-typescript-loader": "2.2.1", + "codelyzer": "0.0.28", + "json-loader": "^0.5.4", + "path": "^0.12.7", + "rimraf": "^2.5.4", + "tap": "7.1.1", + "tslint": "^3.15.1", + "typescript": "2.0.0", + "typings": "^1.3.3" }, "scripts": { + "install": "typings install", + "build": "rimraf ./lib && tsc", + "pretest": "npm run build", "test": "./node_modules/.bin/tap ./tst" }, "license": "MIT" diff --git a/src/ber/index.ts b/src/ber/index.ts new file mode 100644 index 0000000..c82f7fe --- /dev/null +++ b/src/ber/index.ts @@ -0,0 +1,4 @@ +// Copyright 2011 Mark Cavage All rights reserved. + +export { Reader } from './reader'; +export { Writer } from './writer'; diff --git a/src/ber/reader.ts b/src/ber/reader.ts new file mode 100644 index 0000000..d88b180 --- /dev/null +++ b/src/ber/reader.ts @@ -0,0 +1,233 @@ +// Copyright 2011 Mark Cavage All rights reserved. + +import { ASN1, InvalidAsn1Error } from '../common'; + +export class Reader { + private _buf: Buffer; + private _size: number; + + // These hold the "current" state + private _len: number = 0; + private _offset: number = 0; + + constructor(data: Buffer) { + if (!data || !Buffer.isBuffer(data)) + throw new TypeError('data must be a node Buffer'); + + this._buf = data; + this._size = data.length; + } + + public get length(): number { + return this._len; + } + + public get offset(): number { + return this._offset; + } + + public get remain(): number { + return this._size - this._offset; + } + + public get buffer(): Buffer { + return this._buf.slice(this._offset); + } + + /** + * Reads a single byte and advances offset; you can pass in `true` to make this + * a "peek" operation (i.e., get the byte, but don't advance the offset). + * + * @param {Boolean} peek true means don't move offset. + * @return {Number} the next byte, null if not enough data. + */ + public readByte(peek: boolean): number { + if (this._size - this._offset < 1) + return null; + + let b: number = this._buf[this._offset] & 0xff; + + if (!peek) + this._offset += 1; + + return b; + } + + public peek() { + return this.readByte(true); + } + + /** + * Reads a (potentially) variable length off the BER buffer. This call is + * not really meant to be called directly, as callers have to manipulate + * the internal buffer afterwards. + * + * As a result of this call, you can call `Reader.length`, until the + * next thing called that does a readLength. + * + * @return {Number} the amount of offset to advance the buffer. + * @throws {InvalidAsn1Error} on bad ASN.1 + */ + public readLength(offset: any) { + if (offset === undefined) + offset = this._offset; + + if (offset >= this._size) + return null; + + let lenB = this._buf[offset++] & 0xff; + if (lenB === null) + return null; + + if ((lenB & 0x80) === 0x80) { + lenB &= 0x7f; + + if (lenB == 0) + throw new InvalidAsn1Error('Indefinite length not supported'); + + if (lenB > 4) + throw new InvalidAsn1Error('encoding too long'); + + if (this._size - offset < lenB) + return null; + + this._len = 0; + for (let i = 0; i < lenB; i++) + this._len = (this._len << 8) + (this._buf[offset++] & 0xff); + + } else { + // Wasn't a variable length + this._len = lenB; + } + + return offset; + }; + + /** + * Parses the next sequence in this BER buffer. + * + * To get the length of the sequence, call `Reader.length`. + * + * @return {Number} the sequence's tag. + */ + public readSequence(tag: number): number { + let seq = this.peek(); + if (seq === null) + return null; + if (tag !== undefined && tag !== seq) + throw new InvalidAsn1Error('Expected 0x' + tag.toString(16) + ': got 0x' + seq.toString(16)); + + let o = this.readLength(this._offset + 1); // stored in `length` + if (o === null) + return null; + + this._offset = o; + return seq; + }; + + public readInt(): number { + return this._readTag(ASN1.Integer); + }; + + public readBoolean(): boolean { + return this._readTag(ASN1.Boolean) === 0 ? false : true; + }; + + public readEnumeration(): number { + return this._readTag(ASN1.Enumeration); + }; + + public readString(tag?: ASN1, retbuf: Boolean = false): Buffer | string { + if (!tag) + tag = ASN1.OctetString; + + let b = this.peek(); + if (b === null) + return null; + + if (b !== tag) + throw new InvalidAsn1Error('Expected 0x' + tag.toString(16) + ': got 0x' + b.toString(16)); + + let o = this.readLength(this._offset + 1); // stored in `length` + + if (o === null) + return null; + + if (this.length > this._size - o) + return null; + + this._offset = o; + + if (this.length === 0) + return retbuf ? new Buffer(0) : ''; + + let str = this._buf.slice(this._offset, this._offset + this.length); + this._offset += this.length; + + return retbuf ? str : str.toString('utf8'); + }; + + public readOID = function (tag?: ASN1) { + if (!tag) + tag = ASN1.OID; + + let b = this.readString(tag, true); + if (b === null) + return null; + + let values: number[] = []; + let value = 0; + + for (let i = 0; i < b.length; i++) { + let byte = b[i] & 0xff; + + value <<= 7; + value += byte & 0x7f; + if ((byte & 0x80) == 0) { + values.push(value); + value = 0; + } + } + + value = values.shift(); + values.unshift(value % 40); + values.unshift((value / 40) >> 0); + + return values.join('.'); + }; + + private _readTag(tag: ASN1): number { + let b = this.peek(); + + if (b === null) + return null; + + if (b !== tag) + throw new InvalidAsn1Error('Expected 0x' + tag.toString(16) + ': got 0x' + b.toString(16)); + + let o = this.readLength(this._offset + 1); // stored in `length` + if (o === null) + return null; + + if (this.length > 4) + throw new InvalidAsn1Error('Integer too long: ' + this.length); + + if (this.length > this._size - o) + return null; + this._offset = o; + + let fb = this._buf[this._offset]; + let value = 0; + + let i: number = 0; + for (i = 0; i < this.length; i++) { + value <<= 8; + value |= (this._buf[this._offset++] & 0xff); + } + + if ((fb & 0x80) === 0x80 && i !== 4) + value -= (1 << (i * 8)); + + return value >> 0; + } +} diff --git a/src/ber/writer.ts b/src/ber/writer.ts new file mode 100644 index 0000000..42dfd75 --- /dev/null +++ b/src/ber/writer.ts @@ -0,0 +1,298 @@ +// Copyright 2011 Mark Cavage All rights reserved. + +import { ASN1, InvalidAsn1Error } from '../common'; +import * as assert from 'assert'; + +class Options { + public size: number; + public growthFactor: number; +} + +let DEFAULT_OPTS: Options = { + size: 1024, + growthFactor: 8 +}; + +export class Writer { + private _buf: Buffer; + private _size: number; + private _offset: number = 0; + private _options: any = 0; + + // A list of offsets in the buffer where we need to insert sequence tag/len pairs. + private _seq: number[] = []; + + constructor(options: any) { + options = this._merge(DEFAULT_OPTS, options || {}); + + this._buf = new Buffer(options.size || 1024); + this._size = this._buf.length; + this._offset = 0; + this._options = options; + } + + public get buffer(): Buffer { + if (this._seq.length) + throw new InvalidAsn1Error(this._seq.length + ' unended sequence(s)'); + + return (this._buf.slice(0, this._offset)); + } + + public writeLength(len: number): void { + if (typeof (len) !== 'number') + throw new TypeError('argument must be a Number'); + + this._ensure(4); + + if (len <= 0x7f) { + this._buf[this._offset++] = len; + } else if (len <= 0xff) { + this._buf[this._offset++] = 0x81; + this._buf[this._offset++] = len; + } else if (len <= 0xffff) { + this._buf[this._offset++] = 0x82; + this._buf[this._offset++] = len >> 8; + this._buf[this._offset++] = len; + } else if (len <= 0xffffff) { + this._buf[this._offset++] = 0x83; + this._buf[this._offset++] = len >> 16; + this._buf[this._offset++] = len >> 8; + this._buf[this._offset++] = len; + } else { + throw new InvalidAsn1Error('Length too long (> 4 bytes)'); + } + }; + + public writeByte(b: number): void { + if (typeof (b) !== 'number') + throw new TypeError('argument must be a Number'); + + this._ensure(1); + this._buf[this._offset++] = b; + } + + public writeInt(i: number, tag?: ASN1): void { + if (typeof (i) !== 'number') + throw new TypeError('argument must be a Number'); + if (typeof (tag) !== 'number') + tag = ASN1.Integer; + + let sz: number = 4; + + while ((((i & 0xff800000) === 0) || ((i & 0xff800000) === 0xff800000 >> 0)) && (sz > 1)) { + sz--; + i <<= 8; + } + + if (sz > 4) + throw new InvalidAsn1Error('BER ints cannot be > 0xffffffff'); + + this._ensure(2 + sz); + this._buf[this._offset++] = tag; + this._buf[this._offset++] = sz; + + while (sz-- > 0) { + this._buf[this._offset++] = ((i & 0xff000000) >>> 24); + i <<= 8; + } + }; + + public writeNull(): void { + this.writeByte(ASN1.Null); + this.writeByte(0x00); + }; + + public writeEnumeration(i: number, tag?: ASN1): void { + if (typeof (i) !== 'number') + throw new TypeError('argument must be a Number'); + if (typeof (tag) !== 'number') + tag = ASN1.Enumeration; + + return this.writeInt(i, tag); + }; + + public writeBoolean(b: boolean, tag?: ASN1): void { + if (typeof (b) !== 'boolean') + throw new TypeError('argument must be a Boolean'); + if (typeof (tag) !== 'number') + tag = ASN1.Boolean; + + this._ensure(3); + this._buf[this._offset++] = tag; + this._buf[this._offset++] = 0x01; + this._buf[this._offset++] = b ? 0xff : 0x00; + }; + + public writeString(s: string, tag?: ASN1): void { + if (typeof (s) !== 'string') + throw new TypeError('argument must be a string (was: ' + typeof (s) + ')'); + if (typeof (tag) !== 'number') + tag = ASN1.OctetString; + + let len: number = Buffer.byteLength(s); + this.writeByte(tag); + this.writeLength(len); + + if (len) { + this._ensure(len); + this._buf.write(s, this._offset); + this._offset += len; + } + }; + + public writeBuffer(buf: Buffer, tag?: ASN1): void { + if (typeof (tag) !== 'number') + throw new TypeError('tag must be a number'); + if (!Buffer.isBuffer(buf)) + throw new TypeError('argument must be a buffer'); + + this.writeByte(tag); + this.writeLength(buf.length); + this._ensure(buf.length); + buf.copy(this._buf, this._offset, 0, buf.length); + this._offset += buf.length; + }; + + public writeStringArray(strings: string[]): void { + if (!(strings instanceof Array)) + throw new TypeError('argument must be an Array[String]'); + + let self: Writer = this; + strings.forEach(function (s: string) { + self.writeString(s); + }); + }; + + // This is really to solve DER cases, but whatever for now + public writeOID(s: string, tag?: ASN1): void { + if (typeof (s) !== 'string') + throw new TypeError('argument must be a string'); + if (typeof (tag) !== 'number') + tag = ASN1.OID; + + if (!/^([0-9]+\.){3,}[0-9]+$/.test(s)) + throw new Error('argument is not a valid OID string'); + + let tmp: string[] = s.split('.'); + let bytes: number[] = []; + + bytes.push(parseInt(tmp[0], 10) * 40 + parseInt(tmp[1], 10)); + + let self: Writer = this; + tmp.slice(2).forEach(function (b: string) { + self._encodeOctet(bytes, parseInt(b, 10)); + }); + + this._ensure(2 + bytes.length); + this.writeByte(tag); + this.writeLength(bytes.length); + + bytes.forEach(function (b) { + self.writeByte(b); + }); + }; + + public startSequence(tag?: ASN1): void { + if (typeof (tag) !== 'number') + tag = ASN1.Sequence | ASN1.Constructor; + + this.writeByte(tag); + this._seq.push(this._offset); + this._ensure(3); + this._offset += 3; + }; + + public endSequence() { + let seq: number = this._seq.pop(); + let start: number = seq + 3; + let len: number = this._offset - start; + + if (len <= 0x7f) { + this._shift(start, len, -2); + this._buf[seq] = len; + } else if (len <= 0xff) { + this._shift(start, len, -1); + this._buf[seq] = 0x81; + this._buf[seq + 1] = len; + } else if (len <= 0xffff) { + this._buf[seq] = 0x82; + this._buf[seq + 1] = len >> 8; + this._buf[seq + 2] = len; + } else if (len <= 0xffffff) { + this._shift(start, len, 1); + this._buf[seq] = 0x83; + this._buf[seq + 1] = len >> 16; + this._buf[seq + 2] = len >> 8; + this._buf[seq + 3] = len; + } else { + throw new InvalidAsn1Error('Sequence too long'); + } + }; + + private _merge(fromObject: any, toObject: any): any { + assert.ok(fromObject); + assert.equal(typeof (fromObject), 'object'); + assert.ok(toObject); + assert.equal(typeof (toObject), 'object'); + + let keys = Object.getOwnPropertyNames(fromObject); + keys.forEach(function (key) { + if (toObject[key]) + return; + + let value = Object.getOwnPropertyDescriptor(fromObject, key); + Object.defineProperty(toObject, key, value); + }); + + return toObject; + } + + private _shift(start: number, len: number, shift: number): void { + assert.ok(start !== undefined); + assert.ok(len !== undefined); + assert.ok(shift); + + this._buf.copy(this._buf, start + shift, start, start + len); + this._offset += shift; + }; + + private _ensure(len: number) { + assert.ok(len); + + if (this._size - this._offset < len) { + let sz = this._size * this._options.growthFactor; + if (sz - this._offset < len) + sz += len; + + let buf = new Buffer(sz); + + this._buf.copy(buf, 0, 0, this._offset); + this._buf = buf; + this._size = sz; + } + }; + + private _encodeOctet(bytes: number[], octet: number): void { + if (octet < 128) { + bytes.push(octet); + } else if (octet < 16384) { + bytes.push((octet >>> 7) | 0x80); + bytes.push(octet & 0x7F); + } else if (octet < 2097152) { + bytes.push((octet >>> 14) | 0x80); + bytes.push(((octet >>> 7) | 0x80) & 0xFF); + bytes.push(octet & 0x7F); + } else if (octet < 268435456) { + bytes.push((octet >>> 21) | 0x80); + bytes.push(((octet >>> 14) | 0x80) & 0xFF); + bytes.push(((octet >>> 7) | 0x80) & 0xFF); + bytes.push(octet & 0x7F); + } else { + bytes.push(((octet >>> 28) | 0x80) & 0xFF); + bytes.push(((octet >>> 21) | 0x80) & 0xFF); + bytes.push(((octet >>> 14) | 0x80) & 0xFF); + bytes.push(((octet >>> 7) | 0x80) & 0xFF); + bytes.push(octet & 0x7F); + } + } +} diff --git a/src/common/errors.ts b/src/common/errors.ts new file mode 100644 index 0000000..6a74547 --- /dev/null +++ b/src/common/errors.ts @@ -0,0 +1,9 @@ +// Copyright 2011 Mark Cavage All rights reserved. + +export class InvalidAsn1Error extends Error { + + constructor(message: string) { + super(message || ''); + this.name = 'InvalidAsn1Error'; + } +}; diff --git a/src/common/index.ts b/src/common/index.ts new file mode 100644 index 0000000..be4d4f6 --- /dev/null +++ b/src/common/index.ts @@ -0,0 +1,4 @@ +// Copyright 2011 Mark Cavage All rights reserved. + +export { ASN1 } from './types'; +export { InvalidAsn1Error } from './errors'; diff --git a/src/common/types.ts b/src/common/types.ts new file mode 100644 index 0000000..adc012b --- /dev/null +++ b/src/common/types.ts @@ -0,0 +1,38 @@ +// Copyright 2011 Mark Cavage All rights reserved. + +/** + * See http://www.oss.com/asn1/resources/reference/asn1-reference-card.html + */ +export enum ASN1 { + EOC = 0, + Boolean = 1, + Integer = 2, + BitString = 3, + OctetString = 4, + Null = 5, + OID = 6, + ObjectDescriptor = 7, + External = 8, + Real = 9, // float + Enumeration = 10, + PDV = 11, + Utf8String = 12, + RelativeOID = 13, + Sequence = 16, + Set = 17, + NumericString = 18, + PrintableString = 19, + T61String = 20, + VideotexString = 21, + IA5String = 22, + UTCTime = 23, + GeneralizedTime = 24, + GraphicString = 25, + VisibleString = 26, + GeneralString = 28, + UniversalString = 29, + CharacterString = 30, + BMPString = 31, + Constructor = 32, + Context = 128 +}; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..1f362a0 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,11 @@ +// Copyright 2011 Mark Cavage All rights reserved. + +// If you have no idea what ASN.1 or BER is, see this: +// ftp://ftp.rsa.com/pub/pkcs/ascii/layman.asc + +import * as Ber from './ber/index'; + +///--- Exported API +export { ASN1, InvalidAsn1Error } from './common'; +export { Reader as BerReader, Writer as BerWriter } from './ber/index'; +export { Ber as Ber }; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d04807d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2015", + "rootDir": "./src", + "outDir": "./lib", + "removeComments": false, + "noImplicitAny": true, + "sourceMap": false, + "emitDecoratorMetadata": false, + "experimentalDecorators": false, + "moduleResolution": "node", + "declaration": true, + "listFiles": false + }, + "exclude": [ + ".vscode", + ".git", + "node_modules" + ], + "compileOnSave": false, + "buildOnSave": false, + "atom": { + "rewriteTsconfig": false + } +} \ No newline at end of file diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..e820ea2 --- /dev/null +++ b/tslint.json @@ -0,0 +1,137 @@ +{ + "rulesDirectory": [ + "node_modules/codelyzer" + ], + "rules": { + "member-access": false, + "member-ordering": [ + true, + "public-before-private", + "static-before-instance", + "variables-before-functions" + ], + "no-any": false, + "no-inferrable-types": false, + "no-internal-module": true, + "no-var-requires": false, + "typedef": false, + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + }, + { + "call-signature": "space", + "index-signature": "space", + "parameter": "space", + "property-declaration": "space", + "variable-declaration": "space" + } + ], + + "ban": false, + "curly": false, + "forin": true, + "label-position": true, + "label-undefined": true, + "no-arg": true, + "no-bitwise": false, + "no-conditional-assignment": true, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-debugger": true, + "no-duplicate-key": true, + "no-duplicate-variable": true, + "no-empty": false, + "no-eval": true, + "no-null-keyword": false, + "no-shadowed-variable": true, + "no-string-literal": false, + "no-switch-case-fall-through": true, + "no-unreachable": true, + "no-unused-expression": true, + "no-unused-variable": false, + "no-use-before-declare": true, + "no-var-keyword": true, + "radix": true, + "switch-default": true, + "triple-equals": [ + true, + "allow-null-check" + ], + "use-strict": [ + true, + "check-module" + ], + + "eofline": true, + "indent": [ + true + ], + "max-line-length": [ + true, + 200 + ], + "no-require-imports": false, + "no-trailing-whitespace": true, + "object-literal-sort-keys": false, + "trailing-comma": [ + true, + { + "multiline": false, + "singleline": "never" + } + ], + + "align": false, + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "interface-name": false, + "jsdoc-format": true, + "no-consecutive-blank-lines": false, + "no-constructor-vars": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-finally", + "check-whitespace" + ], + "quotemark": [ + true, + "single", + "avoid-escape" + ], + "semicolon": [true, "always"], + "variable-name": [ + true, + "check-format", + "allow-leading-underscore", + "ban-keywords" + ], + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ], + "import-destructuring-spacing": true + } +} diff --git a/tst/.npmignore b/tst/.npmignore new file mode 100644 index 0000000..17f9d0e --- /dev/null +++ b/tst/.npmignore @@ -0,0 +1,4 @@ +*.json +tst +webpack.* +.travis.* \ No newline at end of file diff --git a/tst/ber/writer.test.js b/tst/ber/writer.test.js index d87cb7b..fb1ad73 100644 --- a/tst/ber/writer.test.js +++ b/tst/ber/writer.test.js @@ -1,7 +1,7 @@ // Copyright 2011 Mark Cavage All rights reserved. var test = require('tap').test; -var sys = require('sys'); +var util = require('util'); ///--- Globals @@ -12,13 +12,21 @@ var BerReader; ///--- Tests -test('load library', function(t) { +test('load library (BerWriter)', function(t) { BerWriter = require('../../lib/index').BerWriter; t.ok(BerWriter); t.ok(new BerWriter()); t.end(); }); +test('load library (Ber.Writer)', function(t) { + var Ber = require('../../lib/index').Ber; + + var writer = new Ber.Writer(); + t.ok(writer); + t.end(); +}); + test('write byte', function(t) { var writer = new BerWriter(); @@ -279,7 +287,7 @@ test('sequence', function(t) { var ber = writer.buffer; t.ok(ber); - console.log(ber); + // console.log(ber); t.equal(ber.length, 15, 'wrong length'); t.equal(ber[0], 0x30, 'wrong tag'); t.equal(ber[1], 13, 'wrong length'); @@ -361,10 +369,8 @@ test('Write OID', function(t) { var ber = writer.buffer; t.ok(ber); - console.log(require('util').inspect(ber)); - console.log(require('util').inspect(new Buffer([0x06, 0x09, 0x2a, 0x86, - 0x48, 0x86, 0xf7, 0x0d, - 0x01, 0x01, 0x01]))); + // console.log(util.inspect(ber)); + // console.log(util.inspect(new Buffer([0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01]))); t.end(); }); diff --git a/typings.json b/typings.json new file mode 100644 index 0000000..acb7465 --- /dev/null +++ b/typings.json @@ -0,0 +1,6 @@ +{ + "globalDependencies": {}, + "globalDevDependencies": { + "node-4": "registry:dt/node-4#4.0.0+20160909145701" + } +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..f544412 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,92 @@ +var path = require('path'); + +// Helper functions +var ROOT = path.resolve(__dirname, '..'); + +function root(args) { + args = Array.prototype.slice.call(arguments, 0); + return path.join.apply(path, [ROOT].concat(args)); +} + +module.exports = { + entry: './src/index.ts', + target: 'node', + resolve: { + extensions: ['', '.ts', '.js', '.json'], + + // Make sure root is src + root: '', + + // remove other default values + modulesDirectories: ['node_modules'] + }, + output: { + libraryTarget: 'commonjs', + path: path.join(__dirname, 'webpack'), + filename: 'handler.js' + }, + module: { + loaders: [ + { + test: /\.ts$/, + loaders: [ + 'awesome-typescript-loader' + ], + exclude: [/\.(spec|e2e)\.ts$/] + }, + { + test: /\.json$/, + loader: 'json-loader' + }, + ] + } +}; + + +// //var path = require('path'); + +// module.exports = { +// entry: __dirname + '/src/index.ts', +// target: 'node', +// resolve: { +// extensions: ['', '.ts', '.js', '.json'], + +// // Make sure root is src +// root: __dirname + '/src', + +// // remove other default values +// modulesDirectories: ['node_modules'] +// }, +// output: { +// libraryTarget: 'commonjs', +// path: __dirname + '/webpack', +// filename: 'bundle.js' +// }, +// module: { +// // preLoaders: [ +// // { +// // test: /\.ts$/, +// // loader: 'string-replace-loader', +// // query: { +// // search: '(System|SystemJS)(.*[\\n\\r]\\s*\\.|\\.)import\\((.+)\\)', +// // replace: '$1.import($3).then(mod => mod.__esModule ? mod.default : mod)', +// // flags: 'g' +// // }, +// // include: [__dirname + '/src'] +// // } +// // ], +// loaders: [ +// { +// test: /\.ts$/, +// loaders: [ +// 'awesome-typescript-loader' +// ], +// exclude: [/\.(spec|e2e)\.ts$/] +// }, +// { +// test: /\.json$/, +// loader: 'json-loader' +// }, +// ] +// } +// };