diff --git a/.gitignore b/.gitignore index 740a0a4..1c047ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules *.log package-lock.json +.tsbuildinfo +.vscode/** \ No newline at end of file diff --git a/README.md b/README.md index 2bd8984..d05da5e 100644 --- a/README.md +++ b/README.md @@ -580,6 +580,12 @@ instance: * Improve writeInt() function +## Version 1.2.3 - 29/05/2023 + + * Refactor into Typescript + * Add chai for assert functionality + * Add typescript types + # License Copyright (c) 2020 Mark Abrahams diff --git a/index.js b/index.js deleted file mode 100644 index 57809f4..0000000 --- a/index.js +++ /dev/null @@ -1,6 +0,0 @@ - -var Ber = require('./lib/ber/index') - -exports.Ber = Ber -exports.BerReader = Ber.Reader -exports.BerWriter = Ber.Writer diff --git a/lib/ber/errors.js b/lib/ber/errors.js deleted file mode 100644 index 0106747..0000000 --- a/lib/ber/errors.js +++ /dev/null @@ -1,9 +0,0 @@ - -module.exports = { - InvalidAsn1Error: function(msg) { - var e = new Error() - e.name = 'InvalidAsn1Error' - e.message = msg || '' - return e - } -} diff --git a/lib/ber/index.js b/lib/ber/index.js deleted file mode 100644 index 65985c1..0000000 --- a/lib/ber/index.js +++ /dev/null @@ -1,17 +0,0 @@ - -var errors = require('./errors') -var types = require('./types') - -var Reader = require('./reader') -var Writer = require('./writer') - -for (var t in types) - if (types.hasOwnProperty(t)) - exports[t] = types[t] - -for (var e in errors) - if (errors.hasOwnProperty(e)) - exports[e] = errors[e] - -exports.Reader = Reader -exports.Writer = Writer diff --git a/lib/ber/reader.d.ts b/lib/ber/reader.d.ts new file mode 100644 index 0000000..6629e9a --- /dev/null +++ b/lib/ber/reader.d.ts @@ -0,0 +1,25 @@ +/// +export type NullableNumber = number | null; +export type NullableString = string | null; +export default class Reader { + private _buffer; + private _size; + private _length; + private _offset; + constructor(data: Buffer); + get length(): number; + get offset(): number; + get remain(): number; + get buffer(): Buffer; + private readTag; + readByte: (peek?: boolean) => NullableNumber; + peek: () => NullableNumber; + readLength: (offset: number) => NullableNumber; + readSequence: (tag?: number) => NullableNumber; + readInt: (tag?: number) => NullableNumber; + readBoolean: (tag?: number) => boolean; + readEnumeration: (tag?: number) => NullableNumber; + readString: (tag?: number, returnBuffer?: boolean) => NullableString | Buffer; + readOID: (tag?: number) => NullableString; + readBitString: (tag?: number) => NullableString; +} diff --git a/lib/ber/reader.js b/lib/ber/reader.js index 4a08a0b..945c8d8 100644 --- a/lib/ber/reader.js +++ b/lib/ber/reader.js @@ -1,303 +1,169 @@ - -var assert = require('assert'); - -var ASN1 = require('./types'); -var errors = require('./errors'); - - -///--- Globals - -var InvalidAsn1Error = errors.InvalidAsn1Error; - - - -///--- 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; +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const asn1_types_1 = require("../common/asn1.types"); +const errors_1 = require("../common/errors"); +const validation_1 = require("../common/validation"); +class Reader { + constructor(data) { + this.readTag = (tag) => { + const byte = this.peek(); + if ((0, validation_1.isNil)(byte)) + return null; + if (!(0, validation_1.isNil)(tag) && byte !== tag) { + throw new errors_1.InvalidAsn1Error(`Expected 0x${tag === null || tag === void 0 ? void 0 : tag.toString(16)}, got 0x${byte === null || byte === void 0 ? void 0 : byte.toString(16)}`); + } + const expectedSize = this.readLength(this._offset + 1); + if ((0, validation_1.isNil)(expectedSize)) + return null; + if (this._length === 0) { + throw new errors_1.InvalidAsn1Error('Zero-length integer not supported'); + } + if (this.length > this._size - expectedSize) + return null; + this._offset = expectedSize; + let value = this._buffer.readInt8(this._offset++); + for (let i = 1; i < this.length; i++) { + value *= 256; + value += this._buffer[this._offset++]; + } + if (!Number.isSafeInteger(value)) { + throw new errors_1.InvalidAsn1Error('Integer not respresentable as Javascript number.'); + } + return value; + }; + this.readByte = (peek) => { + if (this.remain < 1) + return null; + const byte = this._buffer[this.offset] & 0xff; + if (!peek) + this._offset++; + return byte; + }; + this.peek = () => this.readByte(true); + this.readLength = (offset) => { + offset = (0, validation_1.isNil)(offset) ? this._offset : offset; + if (offset >= this._size) + return null; + let size = this._buffer[offset++] & 0xff; + if ((0, validation_1.isNil)(size)) + return null; + if ((size & 0x80) === 0x80) { + size &= 0x7f; + if (size === 0) { + throw new errors_1.InvalidAsn1Error('Indefinite length not supported'); + } + if (this._size - offset < size) + return null; + this._length = 0; + for (let i = 0; i < size; i++) { + this._length *= 256; + this._length += (this._buffer[offset++] & 0xff); + } + } + else { + this._length = size; + } + return offset; + }; + this.readSequence = (tag) => { + const sequence = this.peek(); + if ((0, validation_1.isNil)(sequence)) + return null; + if (!(0, validation_1.isNil)(tag) && tag !== sequence) { + throw new errors_1.InvalidAsn1Error(`Expected 0x${tag === null || tag === void 0 ? void 0 : tag.toString(16)}, got 0x${sequence === null || sequence === void 0 ? void 0 : sequence.toString(16)}`); + } + const size = this.readLength(this._offset + 1); + if ((0, validation_1.isNil)(size)) + return null; + this._offset = size; + return sequence; + }; + this.readInt = (tag) => this.readTag(tag); + this.readBoolean = (tag) => { + const tagValue = (0, validation_1.isNumber)(tag) ? tag : asn1_types_1.E_ASN1_TYPES.BOOLEAN; + return (this.readTag(tagValue) === 0 ? false : true); + }; + this.readEnumeration = (tag) => { + return !(0, validation_1.isNumber)(tag) + ? this.readTag(asn1_types_1.E_ASN1_TYPES.ENUMERATION) + : this.readTag(tag); + }; + this.readString = (tag, returnBuffer) => { + const tagValue = (0, validation_1.isNumber)(tag) ? tag : asn1_types_1.E_ASN1_TYPES.OCTET_STRING; + const byte = this.peek(); + if ((0, validation_1.isNil)(byte)) + return null; + if (byte !== tagValue) { + throw new errors_1.InvalidAsn1Error(`Expected 0x${tagValue === null || tagValue === void 0 ? void 0 : tagValue.toString(16)}, got 0x${byte === null || byte === void 0 ? void 0 : byte.toString(16)}`); + } + const expectedSize = this.readLength(this._offset + 1); + if ((0, validation_1.isNil)(expectedSize)) + return null; + if (this.length > this._size - expectedSize) + return null; + this._offset = expectedSize; + if (this.length === 0) { + return returnBuffer ? Buffer.alloc(0) : ''; + } + const str = this._buffer.subarray(this.offset, this.offset + this.length); + this._offset += this.length; + return returnBuffer ? str : str.toString('utf-8'); + }; + this.readOID = (tag) => { + const tagValue = (0, validation_1.isNumber)(tag) ? tag : asn1_types_1.E_ASN1_TYPES.OID; + const stringOrBuffer = this.readString(tagValue, true); + if ((0, validation_1.isNil)(stringOrBuffer)) + return null; + const values = []; + let value = 0; + let byte; + for (let i = 0; i < stringOrBuffer.length; i++) { + byte = stringOrBuffer[i] & 0xff; + value <<= 7; + value += byte & 0x7f; + if ((byte & 0x80) === 0) { + values.push(value >>> 0); + value = 0; + } + } + value = values.shift(); + values.unshift(value % 40); + values.unshift((value / 40) >> 0); + return values.join('.'); + }; + this.readBitString = (tag) => { + let tagValue = (0, validation_1.isNumber)(tag) ? tag : asn1_types_1.E_ASN1_TYPES.BIT_STRING; + const byte = this.peek(); + if ((0, validation_1.isNil)(byte)) + return null; + if (byte !== tagValue) { + throw new errors_1.InvalidAsn1Error(`Expected 0x${tagValue === null || tagValue === void 0 ? void 0 : tagValue.toString(16)}, got 0x${byte === null || byte === void 0 ? void 0 : byte.toString(16)}`); + } + const expectedSize = this.readLength(this._offset + 1); + if ((0, validation_1.isNil)(expectedSize)) + return null; + if (this.length > this._size - expectedSize) + return null; + this._offset = expectedSize; + if (this.length === 0) + return ''; + const ignoredBits = this._buffer[this._offset++]; + const bitStringOctets = this._buffer.subarray(this._offset, this._offset + this.length - 1); + const bitString = (Number.parseInt(bitStringOctets.toString('hex'), 16).toString(2)).padStart(bitStringOctets.length * 8, '0'); + this._offset += this.length - 1; + return bitString.substring(0, bitString.length - ignoredBits); + }; + if (!(0, validation_1.isBuffer)(data)) { + throw new TypeError('data must be a node buffer'); + } + this._buffer = data; + this._size = data.length; + this._length = 0; + this._offset = 0; + } + get length() { return this._length; } + get offset() { return this._offset; } + get remain() { return this._size - this._offset; } + get buffer() { return this._buffer.subarray(this.offset); } } - -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 InvalidAsn1Error('Indefinite length not supported'); - - // Caused problems for node-net-snmp issue #172 - // if (lenB > 4) - // throw InvalidAsn1Error('encoding too long'); - - if (this._size - offset < lenB) - return null; - - this._len = 0; - for (var i = 0; i < lenB; i++) { - this._len *= 256; - this._len += (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 InvalidAsn1Error('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(tag) { - // if (typeof(tag) !== 'number') - // tag = ASN1.Integer; - - return this._readTag(tag); -}; - - -Reader.prototype.readBoolean = function(tag) { - if (typeof(tag) !== 'number') - tag = ASN1.Boolean; - - return (this._readTag(tag) === 0 ? false : true); -}; - - -Reader.prototype.readEnumeration = function(tag) { - if (typeof(tag) !== 'number') - tag = ASN1.Enumeration; - - return this._readTag(tag); -}; - - -Reader.prototype.readString = function(tag, retbuf) { - if (!tag) - tag = ASN1.OctetString; - - var b = this.peek(); - if (b === null) - return null; - - if (b !== tag) - throw InvalidAsn1Error('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 ? Buffer.alloc(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 >>> 0); - value = 0; - } - } - - value = values.shift(); - values.unshift(value % 40); - values.unshift((value / 40) >> 0); - - return values.join('.'); -}; - -Reader.prototype.readBitString = function(tag) { - if (!tag) - tag = ASN1.BitString; - - var b = this.peek(); - if (b === null) - return null; - - if (b !== tag) - throw InvalidAsn1Error('Expected 0x' + tag.toString(16) + - ': got 0x' + b.toString(16)); - - var o = this.readLength(this._offset + 1); - - if (o === null) - return null; - - if (this.length > this._size - o) - return null; - - this._offset = o; - - if (this.length === 0) - return ''; - - var ignoredBits = this._buf[this._offset++]; - - var bitStringOctets = this._buf.slice(this._offset, this._offset + this.length - 1); - var bitString = (parseInt(bitStringOctets.toString('hex'), 16).toString(2)).padStart(bitStringOctets.length * 8, '0'); - this._offset += this.length - 1; - - return bitString.substring(0, bitString.length - ignoredBits); -}; - -Reader.prototype._readTag = function(tag) { - // assert.ok(tag !== undefined); - - var b = this.peek(); - - if (b === null) - return null; - - if (tag !== undefined && b !== tag) - throw InvalidAsn1Error('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 === 0) - throw InvalidAsn1Error('Zero-length integer'); - - if (this.length > this._size - o) - return null; - this._offset = o; - - var value = this._buf.readInt8(this._offset++); - for (var i = 1; i < this.length; i++) { - value *= 256; - value += this._buf[this._offset++]; - } - - if ( ! Number.isSafeInteger(value) ) - throw InvalidAsn1Error('Integer not representable as javascript number'); - - return value; -}; - - - -///--- Exported API - -module.exports = Reader; +exports.default = Reader; diff --git a/lib/ber/types.js b/lib/ber/types.js deleted file mode 100644 index 345824b..0000000 --- a/lib/ber/types.js +++ /dev/null @@ -1,34 +0,0 @@ - -module.exports = { - 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/ber/writer.d.ts b/lib/ber/writer.d.ts new file mode 100644 index 0000000..10594eb --- /dev/null +++ b/lib/ber/writer.d.ts @@ -0,0 +1,30 @@ +/// +export type NullableNumber = number | null; +export type NullableString = string | null; +export type WriterOptions = { + size?: number; + growthFactor?: number; +}; +export default class Writer { + private _buffer; + private size; + private offset; + private sequence; + private opts; + constructor(options?: WriterOptions); + get buffer(): Buffer; + private ensure; + private shift; + writeByte: (byte?: number) => void; + writeInt: (val?: number, tag?: number) => void; + writeNull: () => void; + writeEnumeration: (e: number, tag: number) => void; + writeBoolean: (b?: boolean, tag?: number) => void; + writeString: (s?: string, tag?: number) => void; + writeBuffer: (buffer: Buffer, tag?: number) => void; + writeStringArray: (string: string[], tag?: number) => void; + writeOID: (oid: string, tag?: number) => void; + writeLength: (length: number) => void; + startSequence: (tag?: number) => void; + endSequence: () => void; +} diff --git a/lib/ber/writer.js b/lib/ber/writer.js index 75a538a..1f9f94b 100644 --- a/lib/ber/writer.js +++ b/lib/ber/writer.js @@ -1,311 +1,206 @@ - -var assert = require('assert'); -var ASN1 = require('./types'); -var errors = require('./errors'); - - -///--- Globals - -var InvalidAsn1Error = errors.InvalidAsn1Error; - -var DEFAULT_OPTS = { - size: 1024, - growthFactor: 8 +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const asn1_types_1 = require("../common/asn1.types"); +const errors_1 = require("../common/errors"); +const util_1 = require("../common/util"); +const validation_1 = require("../common/validation"); +const DEFAULT_OPTIONS = { + 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 = Buffer.alloc(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 = []; -} - -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 (!Number.isInteger(i)) - throw new TypeError('argument must be an integer'); - if (typeof(tag) !== 'number') - tag = ASN1.Integer; - - let bytes = []; - while (i < -0x80 || i >= 0x80) { - bytes.push(i & 0xff); - i = Math.floor(i / 0x100); - } - bytes.push(i & 0xff); - - this._ensure(2 + bytes.length); - this._buf[this._offset++] = tag; - this._buf[this._offset++] = bytes.length; - - while (bytes.length) { - this._buf[this._offset++] = bytes.pop(); - } -}; - -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 (!Buffer.isBuffer(buf)) - throw new TypeError('argument must be a buffer'); - - // If no tag is specified we will assume `buf` already contains tag and length - if (typeof(tag) === 'number') { - this.writeByte(tag); - this.writeLength(buf.length); - } - - if ( buf.length > 0 ) { - this._ensure(buf.length); - buf.copy(this._buf, this._offset, 0, buf.length); - this._offset += buf.length; +class Writer { + constructor(options = DEFAULT_OPTIONS) { + this.ensure = (length) => { + if (this.size - this.offset >= length) + return; + let size = this.size * this.opts.growthFactor; + if (size - this.offset < length) + size += length; + const buffer = Buffer.alloc(size); + this._buffer.copy(buffer, 0, 0, this.offset); + this._buffer = buffer; + this.size = size; + }; + this.shift = (start, length, shift) => { + if (!(0, validation_1.isNumber)(start) || !(0, validation_1.isNumber)(length) || !(0, validation_1.isNumber)(shift)) { + throw new errors_1.InvalidAsn1Error(`Invalid shift parameters.`); + } + this._buffer.copy(this._buffer, start + shift, start, start + length); + this.offset += shift; + }; + this.writeByte = (byte) => { + if (!(0, validation_1.isNumber)(byte)) { + throw new errors_1.InvalidAsn1Error('Argument must be a number.'); + } + this.ensure(1); + this._buffer[this.offset++] = byte; + }; + this.writeInt = (val, tag) => { + if (!Number.isInteger(val)) { + throw new TypeError('Argument must be a valid integer'); + } + const tagVal = (0, validation_1.isNumber)(tag) ? tag : asn1_types_1.E_ASN1_TYPES.INTEGER; + let bytes = []; + let i = val; + while (i < -0x80 || i >= 0x80) { + bytes.push(i & 0xff); + i = Math.floor(i / 0x100); + } + bytes.push(i & 0xff); + this.ensure(2 + bytes.length); + this._buffer[this.offset++] = tagVal; + this._buffer[this.offset++] = bytes.length; + for (let i = bytes.length - 1; i >= 0; i--) { + this._buffer[this.offset++] = bytes[i]; + } + }; + this.writeNull = () => { + this.writeByte(asn1_types_1.E_ASN1_TYPES.NULL); + this.writeByte(0x00); + }; + this.writeEnumeration = (e, tag) => { + if (!(0, validation_1.isNumber)(e)) { + throw new TypeError('Argument must be a number'); + } + const tagVal = (0, validation_1.isNumber)(tag) ? tag : asn1_types_1.E_ASN1_TYPES.ENUMERATION; + return this.writeInt(e, tagVal); + }; + this.writeBoolean = (b, tag) => { + if (!(0, validation_1.isBoolean)(b)) { + throw new TypeError('Argument must be boolean'); + } + const tagVal = (0, validation_1.isNumber)(tag) ? tag : asn1_types_1.E_ASN1_TYPES.BOOLEAN; + this.ensure(3); + this._buffer[this.offset++] = tagVal; + this._buffer[this.offset++] = 0x01; + this._buffer[this.offset++] = b ? 0xff : 0x00; + }; + this.writeString = (s, tag) => { + if (!(0, validation_1.isString)(s)) { + throw new TypeError('Argument must be a valid string.'); + } + const tagVal = (0, validation_1.isNumber)(tag) ? tag : asn1_types_1.E_ASN1_TYPES.OCTET_STRING; + const length = Buffer.byteLength(s); + this.writeByte(tagVal); + this.writeLength(length); + if (length) { + this.ensure(length); + this._buffer.write(s, this.offset); + this.offset += length; + } + }; + this.writeBuffer = (buffer, tag) => { + if (!(0, validation_1.isBuffer)(buffer)) { + throw new TypeError('Argument must be valid buffer'); + } + if ((0, validation_1.isNumber)(tag)) { + this.writeByte(tag); + this.writeLength(buffer.length); + } + if (buffer.length > 0) { + this.ensure(buffer.length); + buffer.copy(this._buffer, this.offset, 0, buffer.length); + this.offset += buffer.length; + } + }; + this.writeStringArray = (string, tag) => { + if (!(string instanceof Array)) { + throw new TypeError('Argument must be an Array[String]'); + } + string.forEach((s) => this.writeString(s, tag)); + }; + this.writeOID = (oid, tag) => { + if (!(0, validation_1.isString)(oid)) { + throw new TypeError(`Argument must be of type string`); + } + const tagVal = (0, validation_1.isNumber)(tag) ? tag : asn1_types_1.E_ASN1_TYPES.OID; + if (!(0, validation_1.isOID)(oid)) { + throw new Error('Argument is not a valid OID string'); + } + const oidNodes = oid.split('.'); + const bytes = [ + (Number.parseInt(oidNodes[0], 10) * 40 + Number.parseInt(oidNodes[1], 10)), + ]; + oidNodes.slice(2).forEach((b) => (0, util_1.encodeOctet)(bytes, Number.parseInt(b, 10))); + this.ensure(2 + bytes.length); + this.writeByte(tagVal); + this.writeLength(bytes.length); + bytes.forEach((v) => this.writeByte(v)); + }; + this.writeLength = (length) => { + if (!(0, validation_1.isNumber)(length)) { + throw new TypeError(`Length must be a number`); + } + this.ensure(4); + if (length <= 0x7f) { + this._buffer[this.offset++] = length; + } + else if (length <= 0xff) { + this._buffer[this.offset++] = 0x81; + this._buffer[this.offset++] = length; + } + else if (length <= 0xffff) { + this._buffer[this.offset++] = 0x82; + this._buffer[this.offset++] = length >> 8; + this._buffer[this.offset++] = length; + } + else if (length <= 0xffffff) { + this._buffer[this.offset++] = 0x83; + this._buffer[this.offset++] = length >> 16; + this._buffer[this.offset++] = length >> 8; + this._buffer[this.offset++] = length; + } + else { + throw new errors_1.InvalidAsn1Error('Length too long (> 4 bytes)'); + } + }; + this.startSequence = (tag) => { + const tagVal = (0, validation_1.isNumber)(tag) ? tag : asn1_types_1.E_ASN1_TYPES.SEQUENCE | asn1_types_1.E_ASN1_TYPES.CONSTRUCTOR; + this.writeByte(tagVal); + this.sequence.push(this.offset); + this.ensure(3); + this.offset += 3; + }; + this.endSequence = () => { + const seq = this.sequence.pop(); + const start = seq + 3; + const length = this.offset - start; + if (length <= 0x7f) { + this.shift(start, length, -2); + this._buffer[seq] = length; + } + else if (length <= 0xff) { + this.shift(start, length, -1); + this._buffer[seq] = 0x81; + this._buffer[seq + 1] = length; + } + else if (length <= 0xffff) { + this._buffer[seq] = 0x82; + this._buffer[seq + 1] = length >> 8; + this._buffer[seq + 2] = length; + } + else if (length <= 0xffffff) { + this.shift(start, length, 1); + this._buffer[seq] = 0x83; + this._buffer[seq + 1] = length >> 16; + this._buffer[seq + 2] = length >> 8; + this._buffer[seq + 3] = length; + } + else { + throw new errors_1.InvalidAsn1Error('Sequence too long'); + } + }; + this.opts = (0, util_1.mergeObject)(DEFAULT_OPTIONS, options || {}); + this._buffer = Buffer.alloc(this.opts.size || DEFAULT_OPTIONS.size); + this.size = this._buffer.length; + this.offset = 0; + this.sequence = []; } -}; - - -Writer.prototype.writeStringArray = function(strings, tag) { - if (! (strings instanceof Array)) - throw new TypeError('argument must be an Array[String]'); - - var self = this; - strings.forEach(function(s) { - self.writeString(s, tag); - }); -}; - -// 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]+\.){0,}[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); - } - } - - 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 = Buffer.alloc(sz); - - this._buf.copy(buf, 0, 0, this._offset); - this._buf = buf; - this._size = sz; - } -}; - - - -///--- Exported API - -module.exports = Writer; + get buffer() { + if (this.sequence.length) { + throw new errors_1.InvalidAsn1Error(`${this.sequence.length} unedned sequence(s)`); + } + return this._buffer.subarray(0, this.offset); + } +} +exports.default = Writer; diff --git a/lib/common/asn1.types.d.ts b/lib/common/asn1.types.d.ts new file mode 100644 index 0000000..6e44e58 --- /dev/null +++ b/lib/common/asn1.types.d.ts @@ -0,0 +1,37 @@ +export declare enum E_ASN1_TYPES { + EOC = 0, + BOOLEAN = 1, + INTEGER = 2, + BIT_STRING = 3, + OCTET_STRING = 4, + NULL = 5, + OID = 6, + OBJECT_DESCRIPTOR = 7, + EXTERNAL = 8, + REAL = 9, + ENUMERATION = 10, + PDV = 11, + UTF8_STRING = 12, + RELATIVE_OID = 13, + SEQUENCE = 16, + SET = 17, + NUMERIC_STRING = 18, + PRINTABLE_STRING = 19, + T61_STRING = 20, + VIDEO_TEXT_STRING = 21, + IA5_STRING = 22, + UTC_TIME = 23, + GENERALIZED_TIME = 24, + GRAPHIC_STRING = 25, + VISIBLE_STRING = 26, + GENERAL_STRING = 28, + UNIVERSAL_STRING = 29, + CHARATER_STRING = 30, + BMP_STRING = 31, + CONSTRUCTOR = 32, + CONTEXT = 128 +} +export type BER_ASN1_TYPES = { + [key: string]: E_ASN1_TYPES; +}; +export declare const BER_ASN1_TYPES: BER_ASN1_TYPES; diff --git a/lib/common/asn1.types.js b/lib/common/asn1.types.js new file mode 100644 index 0000000..643713f --- /dev/null +++ b/lib/common/asn1.types.js @@ -0,0 +1,70 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.BER_ASN1_TYPES = exports.E_ASN1_TYPES = void 0; +var E_ASN1_TYPES; +(function (E_ASN1_TYPES) { + E_ASN1_TYPES[E_ASN1_TYPES["EOC"] = 0] = "EOC"; + E_ASN1_TYPES[E_ASN1_TYPES["BOOLEAN"] = 1] = "BOOLEAN"; + E_ASN1_TYPES[E_ASN1_TYPES["INTEGER"] = 2] = "INTEGER"; + E_ASN1_TYPES[E_ASN1_TYPES["BIT_STRING"] = 3] = "BIT_STRING"; + E_ASN1_TYPES[E_ASN1_TYPES["OCTET_STRING"] = 4] = "OCTET_STRING"; + E_ASN1_TYPES[E_ASN1_TYPES["NULL"] = 5] = "NULL"; + E_ASN1_TYPES[E_ASN1_TYPES["OID"] = 6] = "OID"; + E_ASN1_TYPES[E_ASN1_TYPES["OBJECT_DESCRIPTOR"] = 7] = "OBJECT_DESCRIPTOR"; + E_ASN1_TYPES[E_ASN1_TYPES["EXTERNAL"] = 8] = "EXTERNAL"; + E_ASN1_TYPES[E_ASN1_TYPES["REAL"] = 9] = "REAL"; + E_ASN1_TYPES[E_ASN1_TYPES["ENUMERATION"] = 10] = "ENUMERATION"; + E_ASN1_TYPES[E_ASN1_TYPES["PDV"] = 11] = "PDV"; + E_ASN1_TYPES[E_ASN1_TYPES["UTF8_STRING"] = 12] = "UTF8_STRING"; + E_ASN1_TYPES[E_ASN1_TYPES["RELATIVE_OID"] = 13] = "RELATIVE_OID"; + E_ASN1_TYPES[E_ASN1_TYPES["SEQUENCE"] = 16] = "SEQUENCE"; + E_ASN1_TYPES[E_ASN1_TYPES["SET"] = 17] = "SET"; + E_ASN1_TYPES[E_ASN1_TYPES["NUMERIC_STRING"] = 18] = "NUMERIC_STRING"; + E_ASN1_TYPES[E_ASN1_TYPES["PRINTABLE_STRING"] = 19] = "PRINTABLE_STRING"; + E_ASN1_TYPES[E_ASN1_TYPES["T61_STRING"] = 20] = "T61_STRING"; + E_ASN1_TYPES[E_ASN1_TYPES["VIDEO_TEXT_STRING"] = 21] = "VIDEO_TEXT_STRING"; + E_ASN1_TYPES[E_ASN1_TYPES["IA5_STRING"] = 22] = "IA5_STRING"; + E_ASN1_TYPES[E_ASN1_TYPES["UTC_TIME"] = 23] = "UTC_TIME"; + E_ASN1_TYPES[E_ASN1_TYPES["GENERALIZED_TIME"] = 24] = "GENERALIZED_TIME"; + E_ASN1_TYPES[E_ASN1_TYPES["GRAPHIC_STRING"] = 25] = "GRAPHIC_STRING"; + E_ASN1_TYPES[E_ASN1_TYPES["VISIBLE_STRING"] = 26] = "VISIBLE_STRING"; + E_ASN1_TYPES[E_ASN1_TYPES["GENERAL_STRING"] = 28] = "GENERAL_STRING"; + E_ASN1_TYPES[E_ASN1_TYPES["UNIVERSAL_STRING"] = 29] = "UNIVERSAL_STRING"; + E_ASN1_TYPES[E_ASN1_TYPES["CHARATER_STRING"] = 30] = "CHARATER_STRING"; + E_ASN1_TYPES[E_ASN1_TYPES["BMP_STRING"] = 31] = "BMP_STRING"; + E_ASN1_TYPES[E_ASN1_TYPES["CONSTRUCTOR"] = 32] = "CONSTRUCTOR"; + E_ASN1_TYPES[E_ASN1_TYPES["CONTEXT"] = 128] = "CONTEXT"; +})(E_ASN1_TYPES = exports.E_ASN1_TYPES || (exports.E_ASN1_TYPES = {})); +exports.BER_ASN1_TYPES = { + 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/errors.d.ts b/lib/common/errors.d.ts new file mode 100644 index 0000000..ad13e11 --- /dev/null +++ b/lib/common/errors.d.ts @@ -0,0 +1,4 @@ +export declare class InvalidAsn1Error extends Error { + readonly name: string; + constructor(message: string); +} diff --git a/lib/common/errors.js b/lib/common/errors.js new file mode 100644 index 0000000..69e22fc --- /dev/null +++ b/lib/common/errors.js @@ -0,0 +1,10 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.InvalidAsn1Error = void 0; +class InvalidAsn1Error extends Error { + constructor(message) { + super(message); + this.name = InvalidAsn1Error.name; + } +} +exports.InvalidAsn1Error = InvalidAsn1Error; diff --git a/lib/common/util.d.ts b/lib/common/util.d.ts new file mode 100644 index 0000000..aa9b953 --- /dev/null +++ b/lib/common/util.d.ts @@ -0,0 +1,2 @@ +export declare const mergeObject: (from: T, to: T) => T; +export declare const encodeOctet: (bytes: number[], octet: number) => void; diff --git a/lib/common/util.js b/lib/common/util.js new file mode 100644 index 0000000..96098f7 --- /dev/null +++ b/lib/common/util.js @@ -0,0 +1,42 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.encodeOctet = exports.mergeObject = void 0; +const mergeObject = (from, to) => { + const keys = Object.getOwnPropertyNames(from); + keys.forEach((key) => { + if (Object.prototype.hasOwnProperty.call(to, key)) + return; + const val = Object.getOwnPropertyDescriptor(from, key); + Object.defineProperty(to, key, val); + }); + return to; +}; +exports.mergeObject = mergeObject; +const 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.encodeOctet = encodeOctet; diff --git a/lib/common/validation.d.ts b/lib/common/validation.d.ts new file mode 100644 index 0000000..136b057 --- /dev/null +++ b/lib/common/validation.d.ts @@ -0,0 +1,6 @@ +export declare const isNil: (data: T) => boolean; +export declare const isBuffer: (data: T) => boolean; +export declare const isNumber: (data: T) => boolean; +export declare const isBoolean: (data: T) => boolean; +export declare const isString: (data: T) => boolean; +export declare const isOID: (data: string) => boolean; diff --git a/lib/common/validation.js b/lib/common/validation.js new file mode 100644 index 0000000..e6274c1 --- /dev/null +++ b/lib/common/validation.js @@ -0,0 +1,27 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.isOID = exports.isString = exports.isBoolean = exports.isNumber = exports.isBuffer = exports.isNil = void 0; +const isNil = (data) => { + return data === null || data === undefined; +}; +exports.isNil = isNil; +const isBuffer = (data) => { + return !(0, exports.isNil)(data) && Buffer.isBuffer(data); +}; +exports.isBuffer = isBuffer; +const isNumber = (data) => { + return typeof data === 'number' && Number.isFinite(data); +}; +exports.isNumber = isNumber; +const isBoolean = (data) => { + return typeof data === 'boolean'; +}; +exports.isBoolean = isBoolean; +const isString = (data) => { + return typeof data === 'string'; +}; +exports.isString = isString; +const isOID = (data) => { + return /^([0-9]+\.){0,}[0-9]+$/.test(data); +}; +exports.isOID = isOID; diff --git a/lib/index.d.ts b/lib/index.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/lib/index.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..8f1053c --- /dev/null +++ b/lib/index.js @@ -0,0 +1,10 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const reader_1 = __importDefault(require("./ber/reader")); +const writer_1 = __importDefault(require("./ber/writer")); +const errors_1 = require("./common/errors"); +const asn1_types_1 = require("./common/asn1.types"); +module.exports = Object.assign(Object.assign({}, asn1_types_1.BER_ASN1_TYPES), { BerReader: reader_1.default, BerWriter: writer_1.default, InvalidAsn1Error: errors_1.InvalidAsn1Error }); diff --git a/package.json b/package.json index ef99b42..7a634b4 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,23 @@ { "name": "asn1-ber", - "version": "1.2.2", + "version": "1.2.3", "description": "Generate and parse ASN1.BER objects", - "main": "index.js", + "main": "./lib/index.js", + "types":"./lib/index.d.ts", "scripts": { - "test": "mocha" + "test:ts": "mocha --bail --require ts-node/register src/**/*.spec.ts", + "test": "npm run build && mocha --bail", + "build": "tsc" }, "directories": {}, - "dependencies": {}, "devDependencies": { - "mocha": "^10.0.0" + "@types/chai": "^4.3.5", + "@types/mocha": "^10.0.1", + "@types/node": "^20.2.5", + "chai": "^4.3.7", + "mocha": "^10.0.0", + "ts-node": "^10.9.1", + "typescript": "^5.0.4" }, "contributors": [ { diff --git a/src/ber/reader.spec.ts b/src/ber/reader.spec.ts new file mode 100644 index 0000000..e40bb9b --- /dev/null +++ b/src/ber/reader.spec.ts @@ -0,0 +1,296 @@ +import { assert } from 'chai'; +import Reader from './reader'; + +describe("lib/ber/reader.js", function() { + describe("readByte()", function() { + it("can read a value", function() { + const reader = new Reader(Buffer.from([0xde])) + assert.equal(reader.readByte(), 0xde) + }) + }) + + describe("readInt()", function() { + it("can read zero", function() { + const reader = new Reader(Buffer.from([0x02, 0x01, 0x00])) + assert.equal(reader.readInt(), 0) + assert.equal(reader.length, 1) + }) + + it("can read a 1 byte positive integer - lowest", function() { + const reader = new Reader(Buffer.from([0x02, 0x01, 0x01])) + assert.equal(reader.readInt(), 1) + assert.equal(reader.length, 1) + }) + + it("can read a 1 byte positive integer - middle", function() { + const reader = new Reader(Buffer.from([0x02, 0x01, 0x34])) + assert.equal(reader.readInt(), 52) + assert.equal(reader.length, 1) + }) + + it("can read a 1 byte positive integer - highest", function() { + const reader = new Reader(Buffer.from([0x02, 0x01, 0x7f])) + assert.equal(reader.readInt(), 127) + assert.equal(reader.length, 1) + }) + + it("can read a 2 byte positive integer - lowest", function() { + const reader = new Reader(Buffer.from([0x02, 0x02, 0x00, 0x80])) + assert.equal(reader.readInt(), 128) + assert.equal(reader.length, 2) + }) + + it("can read a 2 byte positive integer - middle", function() { + const reader = new Reader(Buffer.from([0x02, 0x02, 0x7e, 0xde])) + assert.equal(reader.readInt(), 0x7ede) + assert.equal(reader.length, 2) + }) + + it("can read a 2 byte positive integer - highest", function() { + const reader = new Reader(Buffer.from([0x02, 0x02, 0x7f, 0xff])) + assert.equal(reader.readInt(), 32767) + assert.equal(reader.length, 2) + }) + + it("can read a 3 byte positive integer - lowest", function() { + const reader = new Reader(Buffer.from([0x02, 0x03, 0x00, 0x80, 0x00])) + assert.equal(reader.readInt(), 32768) + assert.equal(reader.length, 3) + }) + + it("can read a 3 byte positive integer - middle", function() { + const reader = new Reader(Buffer.from([0x02, 0x03, 0x7e, 0xde, 0x03])) + assert.equal(reader.readInt(), 8314371) + assert.equal(reader.length, 3) + }) + + it("can read a 3 byte positive integer - highest", function() { + const reader = new Reader(Buffer.from([0x02, 0x03, 0x7e, 0xde, 0x03])) + assert.equal(reader.readInt(), 0x7ede03) + assert.equal(reader.length, 0x03) + }) + + it("can read a 4 byte positive integer - lowest", function() { + const reader = new Reader(Buffer.from([0x02, 0x04, 0x00, 0x80, 0x00, 0x00])) + assert.equal(reader.readInt(), 8388608) + assert.equal(reader.length, 4) + }) + + it("can read a 4 byte positive integer - middle", function() { + const reader = new Reader(Buffer.from([0x02, 0x04, 0x7e, 0xde, 0x03, 0x01])) + assert.equal(reader.readInt(), 2128478977) + assert.equal(reader.length, 4) + }) + + it("can read a 4 byte positive integer - highest", function() { + const reader = new Reader(Buffer.from([0x02, 0x04, 0x7f, 0xff, 0xff, 0xff])) + assert.equal(reader.readInt(), 2147483647) + assert.equal(reader.length, 4) + }) + + it("can read a 5 byte positive integer - lowest", function() { + const reader = new Reader(Buffer.from([0x02, 0x05, 0x00, 0x80, 0x00, 0x00, 0x00])) + assert.equal(reader.readInt(), 2147483648) + assert.equal(reader.length, 5) + }) + + it("can read a 5 byte positive integer - middle", function() { + const reader = new Reader(Buffer.from([0x02, 0x05, 0x00, 0x8b, 0xde, 0x03, 0x01])) + assert.equal(reader.readInt(), 2346582785) + assert.equal(reader.length, 5) + }) + + it("can read a 5 byte positive integer - highest", function() { + const reader = new Reader(Buffer.from([0x02, 0x05, 0x00, 0xff, 0xff, 0xff, 0xff])) + assert.equal(reader.readInt(), 4294967295) + assert.equal(reader.length, 5) + }) + + it("can read a 1 byte negative integer - lowest", function() { + const reader = new Reader(Buffer.from([0x02, 0x01, 0x80])) + assert.equal(reader.readInt(), -128) + assert.equal(reader.length, 1) + }) + + it("can read a 1 byte negative integer - middle", function() { + const reader = new Reader(Buffer.from([0x02, 0x01, 0xdc])) + assert.equal(reader.readInt(), -36) + assert.equal(reader.length, 1) + }) + + it("can read a 1 byte negative integer - highest", function() { + const reader = new Reader(Buffer.from([0x02, 0x01, 0xff])) + assert.equal(reader.readInt(), -1) + assert.equal(reader.length, 1) + }) + + it("can read a 2 byte negative integer - lowest", function() { + const reader = new Reader(Buffer.from([0x02, 0x02, 0x80, 0x00])) + assert.equal(reader.readInt(), -32768) + assert.equal(reader.length, 2) + }) + + it("can read a 2 byte negative integer - middle", function() { + const reader = new Reader(Buffer.from([0x02, 0x02, 0xc0, 0x4e])) + assert.equal(reader.readInt(), -16306) + assert.equal(reader.length, 2) + }) + + it("can read a 2 byte negative integer - highest", function() { + const reader = new Reader(Buffer.from([0x02, 0x02, 0xff, 0x7f])) + assert.equal(reader.readInt(), -129) + assert.equal(reader.length, 2) + }) + + it("can read a 3 byte negative integer - lowest", function() { + const reader = new Reader(Buffer.from([0x02, 0x03, 0x80, 0x00, 0x00])) + assert.equal(reader.readInt(), -8388608) + assert.equal(reader.length, 3) + }) + + it("can read a 3 byte negative integer - middle", function() { + const reader = new Reader(Buffer.from([0x02, 0x03, 0xff, 0x00, 0x19])) + assert.equal(reader.readInt(), -65511) + assert.equal(reader.length, 3) + }) + + it("can read a 3 byte negative integer - highest", function() { + const reader = new Reader(Buffer.from([0x02, 0x03, 0xff, 0x7f, 0xff])) + assert.equal(reader.readInt(), -32769) + assert.equal(reader.length, 3) + }) + + it("can read a 4 byte negative integer - lowest", function() { + const reader = new Reader(Buffer.from([0x02, 0x04, 0x80, 0x00, 0x00, 0x00])) + assert.equal(reader.readInt(), -2147483648) + assert.equal(reader.length, 4) + }) + + it("can read a 4 byte negative integer - middle", function() { + const reader = new Reader(Buffer.from([0x02, 0x04, 0x91, 0x7c, 0x22, 0x1f])) + assert.equal(reader.readInt(), -1854135777) + assert.equal(reader.length, 4) + }) + + it("can read a 4 byte negative integer - highest", function() { + const reader = new Reader(Buffer.from([0x02, 0x04, 0xff, 0x7f, 0xff, 0xff])) + assert.equal(reader.readInt(), -8388609) + assert.equal(reader.length, 4) + }) + }) + + describe("readBoolean()", function() { + it("can read a true value", function() { + const reader = new Reader(Buffer.from([0x01, 0x01, 0xff])) + assert.equal(reader.readBoolean(), true) + assert.equal(reader.length, 0x01) + }) + + it("can read a false value", function() { + const reader = new Reader(Buffer.from([0x01, 0x01, 0x00])) + assert.equal(reader.readBoolean(), false) + assert.equal(reader.length, 0x01) + }) + }) + + describe("readEnumeration()", function() { + it("can read a value", function() { + const reader = new Reader(Buffer.from([0x0a, 0x01, 0x20])) + assert.equal(reader.readEnumeration(), 0x20, 'wrong value') + assert.equal(reader.length, 0x01, 'wrong length') + }) + }) + + describe("readOID()", function() { + it("does not convert to unsigned", function() { + // Make sure 2887117176 is NOT converted to -1407850120 + const buffer = Buffer.from([6, 18, 43, 6, 1, 4, 1, 245, 12, 1, 1, 5, 1, 1, 19, 138, 224, 215, 210, 120]) + const reader = new Reader(buffer) + assert.equal(reader.readOID(), "1.3.6.1.4.1.14988.1.1.5.1.1.19.2887117176") + assert.equal(reader.length, 18) + }) + }) + + describe("readString()", function() { + it("can read a value", function() { + const string = 'cn=foo,ou=unit,o=test' + const buffer = Buffer.alloc(string.length + 2) + buffer[0] = 0x04 + buffer[1] = Buffer.byteLength(string) + buffer.write(string, 2) + + const reader = new Reader(buffer) + assert.equal(reader.readString(), string) + assert.equal(reader.length, string.length) + }) + }) + + describe("readSequence()", function() { + it("can read a sequence", function() { + const reader = new Reader(Buffer.from([0x30, 0x03, 0x01, 0x01, 0xff])) + assert.equal(reader.readSequence(), 0x30) + assert.equal(reader.length, 0x03) + assert.equal(reader.readBoolean(), true) + assert.equal(reader.length, 0x01) + }) + }) + + describe("readBitString()", function() { + it("can read a bit string", function() { + const reader = new Reader(Buffer.from([0x03, 0x07, 0x04, 0x0a, 0x3b, 0x5f, 0x29, 0x1c, 0xd0])) + assert.equal(reader.readBitString(), '00001010001110110101111100101001000111001101') + assert.equal(reader.length, 7) + }) + }) + + describe("complex sequences", function() { + it("are processed correctly", function() { + const buffer = Buffer.alloc(14); + + // An anonymous LDAP v3 BIND request + buffer[0] = 0x30 // Sequence + buffer[1] = 12 // len + buffer[2] = 0x02 // ASN.1 Integer + buffer[3] = 1 // len + buffer[4] = 0x04 // msgid (make up 4) + buffer[5] = 0x60 // Bind Request + buffer[6] = 7 // len + buffer[7] = 0x02 // ASN.1 Integer + buffer[8] = 1 // len + buffer[9] = 0x03 // v3 + buffer[10] = 0x04 // String (bind dn) + buffer[11] = 0 // len + buffer[12] = 0x80 // ContextSpecific (choice) + buffer[13] = 0 // simple bind + + const reader = new Reader(buffer) + assert.equal(reader.readSequence(), 48) + assert.equal(reader.length, 12) + assert.equal(reader.readInt(), 4) + assert.equal(reader.readSequence(), 96) + assert.equal(reader.length, 7) + assert.equal(reader.readInt(), 3) + assert.equal(reader.readString(), "") + assert.equal(reader.length, 0) + assert.equal(reader.readByte(), 0x80) + assert.equal(reader.readByte(), 0) + assert.equal(null, reader.readByte()) + }) + }) + + describe("long strings", function() { + it("can be parsed", function() { + const buffer = Buffer.alloc(256) + const string = "2;649;CN=Red Hat CS 71GA Demo,O=Red Hat CS 71GA Demo,C=US;" + + "CN=RHCS Agent - admin01,UID=admin01,O=redhat,C=US [1] This is " + + "Teena Vradmin's description." + buffer[0] = 0x04 + buffer[1] = 0x81 + buffer[2] = 0x94 + buffer.write(string, 3) + + const reader = new Reader(buffer.slice(0, 3 + string.length)); + assert.equal(reader.readString(), string) + }) + }) +}) diff --git a/src/ber/reader.ts b/src/ber/reader.ts new file mode 100644 index 0000000..469333b --- /dev/null +++ b/src/ber/reader.ts @@ -0,0 +1,205 @@ +import { E_ASN1_TYPES } from '../common/asn1.types'; +import { InvalidAsn1Error } from "../common/errors"; +import { isBuffer, isNil, isNumber } from '../common/validation'; + +export type NullableNumber = number|null; +export type NullableString = string|null; + +export default class Reader { + private _buffer: Buffer; + private _size: number; + // @NOTE: holders current state + private _length: number; + private _offset: number; + constructor(data: Buffer) { + if (!isBuffer(data)) { + throw new TypeError('data must be a node buffer'); + } + this._buffer = data; + this._size = data.length; + this._length = 0; + this._offset = 0; + } + + get length(): number { return this._length; } + get offset(): number { return this._offset; } + get remain(): number { return this._size - this._offset; } + get buffer(): Buffer { return this._buffer.subarray(this.offset); } + + private readTag = (tag?: number): NullableNumber => { + const byte = this.peek(); + + if (isNil(byte)) return null; + + if (!isNil(tag) && byte !== tag) { + throw new InvalidAsn1Error(`Expected 0x${tag?.toString(16)}, got 0x${byte?.toString(16)}`); + } + + const expectedSize = this.readLength(this._offset + 1); + + if (isNil(expectedSize)) return null; + + if (this._length === 0) { + throw new InvalidAsn1Error('Zero-length integer not supported'); + } + + if (this.length > this._size - expectedSize!) return null; + + this._offset = expectedSize!; + + let value = this._buffer.readInt8(this._offset++); + + for (let i = 1; i < this.length; i++) { + value *= 256; + value += this._buffer[this._offset++]; + } + + if (!Number.isSafeInteger(value)) { + throw new InvalidAsn1Error('Integer not respresentable as Javascript number.'); + } + return value; + } + + public readByte = (peek?: boolean): NullableNumber => { + if (this.remain < 1) return null; + const byte = this._buffer[this.offset] & 0xff; + if (!peek) this._offset++; + return byte; + } + + public peek = (): NullableNumber => this.readByte(true); + + public readLength = (offset: number): NullableNumber => { + offset = isNil(offset) ? this._offset : offset; + + if (offset >= this._size) return null; + + let size = this._buffer[offset++] & 0xff; + + if (isNil(size)) return null; + + if ((size & 0x80) === 0x80) { + size &= 0x7f; + + if (size === 0) { + throw new InvalidAsn1Error('Indefinite length not supported'); + } + + if (this._size - offset < size) return null; + + this._length = 0; + + for (let i = 0; i < size; i++) { + this._length *= 256; + this._length += (this._buffer[offset++] & 0xff); + } + } else { + this._length = size; + } + + return offset; + } + + public readSequence = (tag?: number): NullableNumber => { + const sequence = this.peek(); + + if (isNil(sequence)) return null; + + if (!isNil(tag) && tag !== sequence) { + throw new InvalidAsn1Error(`Expected 0x${tag?.toString(16)}, got 0x${sequence?.toString(16)}`); + } + + const size = this.readLength(this._offset + 1); + + if (isNil(size)) return null; + + this._offset = (size as number); + + return sequence; + } + + public readInt = (tag?: number): NullableNumber => this.readTag(tag); + + public readBoolean = (tag?: number): boolean => { + const tagValue = isNumber(tag) ? tag : E_ASN1_TYPES.BOOLEAN; + return (this.readTag(tagValue) === 0 ? false : true); + } + + public readEnumeration = (tag?: number): NullableNumber => { + return !isNumber(tag) + ? this.readTag(E_ASN1_TYPES.ENUMERATION) + : this.readTag(tag); + } + + public readString = (tag?: number, returnBuffer?: boolean): NullableString|Buffer => { + const tagValue = isNumber(tag) ? tag : E_ASN1_TYPES.OCTET_STRING; + const byte = this.peek(); + if (isNil(byte)) return null; + if (byte !== tagValue) { + throw new InvalidAsn1Error(`Expected 0x${tagValue?.toString(16)}, got 0x${byte?.toString(16)}`); + } + + const expectedSize = this.readLength(this._offset + 1); + if (isNil(expectedSize)) return null; + + if (this.length > this._size - expectedSize!) return null; + + this._offset = expectedSize!; + + if (this.length === 0) { + return returnBuffer ? Buffer.alloc(0): ''; + } + const str = this._buffer.subarray(this.offset, this.offset + this.length); + this._offset += this.length; + return returnBuffer ? str : str.toString('utf-8'); + } + + public readOID = (tag?: number): NullableString => { + const tagValue = isNumber(tag) ? tag : E_ASN1_TYPES.OID; + const stringOrBuffer = this.readString(tagValue!, true); + if (isNil(stringOrBuffer)) return null; + const values = []; + let value = 0; + let byte: number; + for (let i = 0; i < stringOrBuffer!.length; i++) { + byte = (stringOrBuffer as Buffer)[i] & 0xff; + value <<= 7; + value += byte & 0x7f; + if ((byte & 0x80) === 0) { + values.push(value >>> 0); + value = 0; + } + } + + value = values.shift()!; + values.unshift(value % 40); + values.unshift((value / 40) >> 0); + return values.join('.'); + } + + public readBitString = (tag?: number): NullableString => { + let tagValue = isNumber(tag) ? tag : E_ASN1_TYPES.BIT_STRING; + + const byte = this.peek(); + + if (isNil(byte)) return null; + + if (byte !== tagValue) { + throw new InvalidAsn1Error(`Expected 0x${tagValue?.toString(16)}, got 0x${byte?.toString(16)}`); + } + const expectedSize = this.readLength(this._offset + 1); + if (isNil(expectedSize)) return null; + if (this.length > this._size - expectedSize!) return null; + + this._offset = expectedSize!; + + if (this.length === 0) return ''; + + const ignoredBits = this._buffer[this._offset++]; + const bitStringOctets = this._buffer.subarray(this._offset, this._offset + this.length - 1); + const bitString = (Number.parseInt( bitStringOctets.toString('hex'), 16).toString(2)).padStart(bitStringOctets.length * 8, '0'); + this._offset += this.length - 1; + + return bitString.substring(0, bitString.length - ignoredBits); + } +} \ No newline at end of file diff --git a/src/ber/writer.spec.ts b/src/ber/writer.spec.ts new file mode 100644 index 0000000..ff597a4 --- /dev/null +++ b/src/ber/writer.spec.ts @@ -0,0 +1,584 @@ +import { assert } from 'chai'; +import Writer from './writer'; + +describe("lib/ber/writer.js", function() { + describe("writeByte()", function() { + it("can write a value", function() { + const writer = new Writer() + writer.writeByte(0xC2) + + const buffer = writer.buffer + + assert.equal(buffer.length, 1) + assert.equal(buffer[0], 0xc2) + }) + }) + + describe("writeInt()", function() { + it("can write zero", function() { + const writer = new Writer() + writer.writeInt(0) + + const buffer = writer.buffer + + assert.equal(buffer.length, 3) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x01) + assert.equal(buffer[2], 0x00) + }) + + it("can write 1 byte positive integers - lowest", function() { + const writer = new Writer() + writer.writeInt(1) + + const buffer = writer.buffer + + assert.equal(buffer.length, 3) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x01) + assert.equal(buffer[2], 0x01) + }) + + it("can write 1 byte positive integers - middle", function() { + const writer = new Writer() + writer.writeInt(101) + + const buffer = writer.buffer + + assert.equal(buffer.length, 3) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x01) + assert.equal(buffer[2], 0x65) + }) + + it("can write 1 byte positive integers - highest", function() { + const writer = new Writer() + writer.writeInt(127) + + const buffer = writer.buffer + + assert.equal(buffer.length, 3) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x01) + assert.equal(buffer[2], 0x7f) + }) + + it("can write 2 byte positive integers - lowest", function() { + const writer = new Writer() + writer.writeInt(128) + + const buffer = writer.buffer + + assert.equal(buffer.length, 4) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x02) + assert.equal(buffer[2], 0x00) + assert.equal(buffer[3], 0x80) + }) + + it("can write 2 byte positive integers - middle", function() { + const writer = new Writer() + writer.writeInt(9374) + + const buffer = writer.buffer + + assert.equal(buffer.length, 4) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x02) + assert.equal(buffer[2], 0x24) + assert.equal(buffer[3], 0x9e) + }) + + it("can write 2 byte positive integers - highest", function() { + const writer = new Writer() + writer.writeInt(32767) + + const buffer = writer.buffer + + assert.equal(buffer.length, 4) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x02) + assert.equal(buffer[2], 0x7f) + assert.equal(buffer[3], 0xff) + }) + + it("can write 3 byte positive integers - lowest", function() { + const writer = new Writer() + writer.writeInt(32768) + + const buffer = writer.buffer + + assert.equal(buffer.length, 5) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x03) + assert.equal(buffer[2], 0x00) + assert.equal(buffer[3], 0x80) + assert.equal(buffer[4], 0x00) + }) + + it("can write 3 byte positive integers - middle", function() { + const writer = new Writer() + writer.writeInt(5938243) + + const buffer = writer.buffer + + assert.equal(buffer.length, 5) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x03) + assert.equal(buffer[2], 0x5a) + assert.equal(buffer[3], 0x9c) + assert.equal(buffer[4], 0x43) + }) + + it("can write 3 byte positive integers - highest", function() { + const writer = new Writer() + writer.writeInt(8388607) + + const buffer = writer.buffer + + assert.equal(buffer.length, 5) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x03) + assert.equal(buffer[2], 0x7f) + assert.equal(buffer[3], 0xff) + assert.equal(buffer[4], 0xff) + }) + + it("can write 4 byte positive integers - lowest", function() { + const writer = new Writer() + writer.writeInt(8388608) + + const buffer = writer.buffer + + assert.equal(buffer.length, 6) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x04) + assert.equal(buffer[2], 0x00) + assert.equal(buffer[3], 0x80) + assert.equal(buffer[4], 0x00) + assert.equal(buffer[5], 0x00) + }) + + it("can write 4 byte positive integers - middle", function() { + const writer = new Writer() + writer.writeInt(1483722690) + + const buffer = writer.buffer + + assert.equal(buffer.length, 6) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x04) + assert.equal(buffer[2], 0x58) + assert.equal(buffer[3], 0x6f) + assert.equal(buffer[4], 0xcf) + assert.equal(buffer[5], 0xc2) + }) + + it("can write 4 byte positive integers - highest", function() { + const writer = new Writer() + writer.writeInt(2147483647) + + const buffer = writer.buffer + + assert.equal(buffer.length, 6) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x04) + assert.equal(buffer[2], 0x7f) + assert.equal(buffer[3], 0xff) + assert.equal(buffer[4], 0xff) + assert.equal(buffer[5], 0xff) + }) + + it("can write 5 byte positive integers - lowest", function() { + const writer = new Writer() + writer.writeInt(2147483648) + + const buffer = writer.buffer + + assert.equal(buffer.length, 7) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x05) + assert.equal(buffer[2], 0x00) + assert.equal(buffer[3], 0x80) + assert.equal(buffer[4], 0x00) + assert.equal(buffer[5], 0x00) + assert.equal(buffer[6], 0x00) + }) + + it("can write 5 byte positive integers - middle", function() { + const writer = new Writer() + writer.writeInt(3843548325) + + const buffer = writer.buffer + + assert.equal(buffer.length, 7) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x05) + assert.equal(buffer[2], 0x00) + assert.equal(buffer[3], 0xe5) + assert.equal(buffer[4], 0x17) + assert.equal(buffer[5], 0xe4) + assert.equal(buffer[6], 0xa5) + }) + + it("can write 5 byte positive integers - highest", function() { + const writer = new Writer() + writer.writeInt(4294967295) + + const buffer = writer.buffer + + assert.equal(buffer.length, 7) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x05) + assert.equal(buffer[2], 0x00) + assert.equal(buffer[3], 0xff) + assert.equal(buffer[4], 0xff) + assert.equal(buffer[5], 0xff) + assert.equal(buffer[6], 0xff) + }) + + it("can write 1 byte negative integers - lowest", function() { + const writer = new Writer() + writer.writeInt(-128) + + const buffer = writer.buffer + + assert.equal(buffer.length, 3) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x01) + assert.equal(buffer[2], 0x80) + }) + + it("can write 1 byte negative integers - middle", function() { + const writer = new Writer() + writer.writeInt(-73) + + const buffer = writer.buffer + + assert.equal(buffer.length, 3) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x01) + assert.equal(buffer[2], 0xb7) + }) + + it("can write 1 byte negative integers - highest", function() { + const writer = new Writer() + writer.writeInt(-1) + + const buffer = writer.buffer + + assert.equal(buffer.length, 3) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x01) + assert.equal(buffer[2], 0xff) + }) + + it("can write 2 byte negative integers - lowest", function() { + const writer = new Writer() + writer.writeInt(-32768) + + const buffer = writer.buffer + + assert.equal(buffer.length, 4) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x02) + assert.equal(buffer[2], 0x80) + assert.equal(buffer[3], 0x00) + }) + + it("can write 2 byte negative integers - middle", function() { + const writer = new Writer() + writer.writeInt(-22400) + + const buffer = writer.buffer + + assert.equal(buffer.length, 4) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x02) + assert.equal(buffer[2], 0xa8) + assert.equal(buffer[3], 0x80) + }) + + it("can write 2 byte negative integers - highest", function() { + const writer = new Writer() + writer.writeInt(-129) + + const buffer = writer.buffer + + assert.equal(buffer.length, 4) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x02) + assert.equal(buffer[2], 0xff) + assert.equal(buffer[3], 0x7f) + }) + + it("can write 3 byte negative integers - lowest", function() { + const writer = new Writer() + writer.writeInt(-8388608) + + const buffer = writer.buffer + + assert.equal(buffer.length, 5) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x03) + assert.equal(buffer[2], 0x80) + assert.equal(buffer[3], 0x00) + assert.equal(buffer[4], 0x00) + }) + + it("can write 3 byte negative integers - middle", function() { + const writer = new Writer() + writer.writeInt(-481653) + + const buffer = writer.buffer + + assert.equal(buffer.length, 5) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x03) + assert.equal(buffer[2], 0xf8) + assert.equal(buffer[3], 0xa6) + assert.equal(buffer[4], 0x8b) + }) + + it("can write 3 byte negative integers - highest", function() { + const writer = new Writer() + writer.writeInt(-32769) + + const buffer = writer.buffer + + assert.equal(buffer.length, 5) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x03) + assert.equal(buffer[2], 0xff) + assert.equal(buffer[3], 0x7f) + assert.equal(buffer[4], 0xff) + }) + + it("can write 4 byte negative integers - lowest", function() { + const writer = new Writer() + writer.writeInt(-2147483648) + + const buffer = writer.buffer + + assert.equal(buffer.length, 6) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x04) + assert.equal(buffer[2], 0x80) + assert.equal(buffer[3], 0x00) + assert.equal(buffer[4], 0x00) + assert.equal(buffer[5], 0x00) + }) + + it("can write 4 byte negative integers - middle", function() { + const writer = new Writer() + writer.writeInt(-1522904131) + + const buffer = writer.buffer + + assert.equal(buffer.length, 6) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x04) + assert.equal(buffer[2], 0xa5) + assert.equal(buffer[3], 0x3a) + assert.equal(buffer[4], 0x53) + assert.equal(buffer[5], 0xbd) + }) + + it("can write 4 byte negative integers - highest", function() { + const writer = new Writer() + writer.writeInt(-8388609) + + const buffer = writer.buffer + + assert.equal(buffer.length, 6) + assert.equal(buffer[0], 0x02) + assert.equal(buffer[1], 0x04) + assert.equal(buffer[2], 0xff) + assert.equal(buffer[3], 0x7f) + assert.equal(buffer[4], 0xff) + assert.equal(buffer[5], 0xff) + }) + }) + + describe("writeBoolean()", function() { + it("can write a true and false value", function() { + const writer = new Writer() + writer.writeBoolean(true) + writer.writeBoolean(false) + + const buffer = writer.buffer + + assert.equal(buffer.length, 6) + assert.equal(buffer[0], 0x01) + assert.equal(buffer[1], 0x01) + assert.equal(buffer[2], 0xff) + assert.equal(buffer[3], 0x01) + assert.equal(buffer[4], 0x01) + assert.equal(buffer[5], 0x00) + }) + }) + + describe("writeString()", function() { + it("can write a value", function() { + const writer = new Writer() + writer.writeString("hello world") + + const buffer = writer.buffer + + assert.equal(buffer.length, 13) + assert.equal(buffer[0], 0x04) + assert.equal(buffer[1], 11) + assert.equal(buffer.slice(2).toString("utf8"), "hello world") + }) + }) + + describe("writeBuffer()", function() { + it("can write a value", function() { + const writer = new Writer() + writer.writeString("hello world") + + const expected = Buffer.from([ + 0x04, 0x0b, 0x30, 0x09, 0x02, 0x01, 0x0f, 0x01, + 0x01, 0xff, 0x01, 0x01, 0xff + ]) + writer.writeBuffer(expected.slice(2, expected.length), 0x04) + + const buffer = writer.buffer; + + assert.equal(buffer.length, 26) + assert.equal(buffer[0], 0x04) + assert.equal(buffer[1], 11) + assert.equal(buffer.subarray(2, 13).toString("utf8"), "hello world") + assert.equal(buffer[13], expected[0]) + assert.equal(buffer[14], expected[1]) + + for (let i = 13, j = 0; i < buffer.length && j < expected.length; i++, j++) + assert.equal(buffer[i], expected[j]) + }) + }) + + describe("writeStringArray()", function() { + it("can write an array of strings", function() { + const writer = new Writer() + writer.writeStringArray(["hello world", "fubar!"]) + + const buffer = writer.buffer + + assert.equal(buffer.length, 21) + assert.equal(buffer[0], 0x04) + assert.equal(buffer[1], 11) + assert.equal(buffer.subarray(2, 13).toString("utf8"), "hello world") + + assert.equal(buffer[13], 0x04) + assert.equal(buffer[14], 6) + assert.equal(buffer.subarray(15).toString("utf8"), "fubar!") + }) + }) + + describe("oversized data", function() { + it("results in a buffer resize", function() { + const writer = new Writer({size: 2}) + writer.writeString("hello world") + + const buffer = writer.buffer + + assert.equal(buffer.length, 13) + assert.equal(buffer[0], 0x04) + assert.equal(buffer[1], 11) + assert.equal(buffer.subarray(2).toString("utf8"), "hello world") + }) + }) + + describe("complex sequences", function() { + it("are processed correctly", function() { + const writer = new Writer({size: 25}) + writer.startSequence() + writer.writeString("hello world") + writer.endSequence() + + const buffer = writer.buffer + + assert.equal(buffer.length, 15) + assert.equal(buffer[0], 0x30) + assert.equal(buffer[1], 13) + assert.equal(buffer[2], 0x04) + assert.equal(buffer[3], 11) + assert.equal(buffer.subarray(4).toString("utf8"), "hello world") + }) + }) + + describe("nested sequences", function() { + it("are processed correctly", function() { + const writer = new Writer({size: 25}) + writer.startSequence() + writer.writeString("hello world") + writer.startSequence() + writer.writeString("hello world") + writer.endSequence() + writer.endSequence() + + const buffer = writer.buffer + + assert.equal(buffer.length, 30) + assert.equal(buffer[0], 0x30) + assert.equal(buffer[1], 28) + assert.equal(buffer[2], 0x04) + assert.equal(buffer[3], 11) + assert.equal(buffer.subarray(4, 15).toString("utf8"), "hello world") + + assert.equal(buffer[15], 0x30) + assert.equal(buffer[16], 13) + assert.equal(buffer[17], 0x04) + assert.equal(buffer[18], 11) + assert.equal(buffer.subarray(19, 30).toString("utf8"), "hello world") + }) + }) + + describe("multiple sequences", function() { + it("are processed correctly", function() { + // An anonymous LDAP v3 BIND request + const dn = "cn=foo,ou=unit,o=test" + + const writer = new Writer() + writer.startSequence() + writer.writeInt(3) + writer.startSequence(0x60) + writer.writeInt(3) + writer.writeString(dn) + writer.writeByte(0x80) + writer.writeByte(0x00) + writer.endSequence() + writer.endSequence() + + const buffer = writer.buffer + + assert.equal(buffer.length, 35) + assert.equal(buffer[0], 0x30) + assert.equal(buffer[1], 33) + assert.equal(buffer[2], 0x02) + assert.equal(buffer[3], 1) + assert.equal(buffer[4], 0x03) + assert.equal(buffer[5], 0x60) + assert.equal(buffer[6], 28) + assert.equal(buffer[7], 0x02) + assert.equal(buffer[8], 1) + assert.equal(buffer[9], 0x03) + assert.equal(buffer[10], 0x04) + assert.equal(buffer[11], dn.length) + assert.equal(buffer.subarray(12, 33).toString("utf8"), dn) + assert.equal(buffer[33], 0x80) + assert.equal(buffer[34], 0x00) + }) + }) + + describe("writeOID()", function() { + it("can write a value", function() { + const writer = new Writer() + writer.writeOID("1.2.840.113549.1.1.1") + + const buffer = writer.buffer + + assert.equal(buffer.toString("hex"), "06092a864886f70d010101") + }) + }) +}) diff --git a/src/ber/writer.ts b/src/ber/writer.ts new file mode 100644 index 0000000..e354505 --- /dev/null +++ b/src/ber/writer.ts @@ -0,0 +1,237 @@ +import { E_ASN1_TYPES } from '../common/asn1.types'; +import { InvalidAsn1Error } from '../common/errors'; +import { encodeOctet, mergeObject } from '../common/util'; +import { isBoolean, isBuffer, isNumber, isOID, isString } from '../common/validation'; + +export type NullableNumber = number|null; +export type NullableString = string|null; + +export type WriterOptions = { + size?: number; + growthFactor?: number; +}; + +const DEFAULT_OPTIONS: WriterOptions = { + size: 1024, + growthFactor: 8, +}; + +export default class Writer { + private _buffer: Buffer; + private size: number; + private offset: number; + private sequence: number[]; + private opts: WriterOptions; + constructor(options: WriterOptions = DEFAULT_OPTIONS) { + this.opts = mergeObject(DEFAULT_OPTIONS, options || {}); + this._buffer = Buffer.alloc(this.opts.size! || DEFAULT_OPTIONS.size!); + this.size = this._buffer.length; + this.offset = 0; + this.sequence = []; + } + + get buffer():Buffer { + if (this.sequence.length) { + throw new InvalidAsn1Error(`${this.sequence.length} unedned sequence(s)`); + } + return this._buffer.subarray(0, this.offset); + } + + private ensure = (length: number): void => { + if (this.size - this.offset >= length) return; + let size = this.size * this.opts.growthFactor!; + if (size - this.offset < length) size += length; + const buffer = Buffer.alloc(size); + this._buffer.copy(buffer, 0, 0, this.offset); + this._buffer = buffer; + this.size = size; + } + + private shift = (start: number, length: number, shift: number): void => { + if (!isNumber(start) || !isNumber(length) || !isNumber(shift)) { + throw new InvalidAsn1Error(`Invalid shift parameters.`); + } + this._buffer.copy(this._buffer, start + shift, start, start + length); + this.offset += shift; + } + + public writeByte = (byte?: number): void => { + if (!isNumber(byte)) { + throw new InvalidAsn1Error('Argument must be a number.'); + } + this.ensure(1); + this._buffer[this.offset++] = byte!; + } + + public writeInt = (val?: number, tag?: number): void => { + if (!Number.isInteger(val)) { + throw new TypeError('Argument must be a valid integer'); + } + const tagVal = isNumber(tag) ? tag : E_ASN1_TYPES.INTEGER; + let bytes = []; + let i = val!; + while (i < -0x80 || i >= 0x80) { + bytes.push(i & 0xff); + i = Math.floor(i / 0x100); + } + bytes.push(i & 0xff); + + this.ensure(2 + bytes.length); + this._buffer[this.offset++] = tagVal!; + this._buffer[this.offset++] = bytes.length; + + for (let i = bytes.length - 1; i >= 0; i--) { + this._buffer[this.offset++] = bytes[i]; + } + } + + public writeNull = (): void => { + this.writeByte(E_ASN1_TYPES.NULL); + this.writeByte(0x00); + } + + public writeEnumeration = (e: number, tag: number): void => { + if (!isNumber(e)) { + throw new TypeError('Argument must be a number'); + } + const tagVal = isNumber(tag) ? tag: E_ASN1_TYPES.ENUMERATION; + return this.writeInt(e, tagVal); + } + + public writeBoolean = (b?: boolean, tag?: number): void => { + if (!isBoolean(b)) { + throw new TypeError('Argument must be boolean'); + } + const tagVal = isNumber(tag) ? tag : E_ASN1_TYPES.BOOLEAN; + this.ensure(3); + this._buffer[this.offset++] = tagVal!; + this._buffer[this.offset++] = 0x01; + this._buffer[this.offset++] = b ? 0xff : 0x00; + } + + public writeString = (s?: string, tag?: number): void => { + if (!isString(s)) { + throw new TypeError('Argument must be a valid string.'); + } + const tagVal = isNumber(tag) ? tag : E_ASN1_TYPES.OCTET_STRING; + const length = Buffer.byteLength(s!); + this.writeByte(tagVal); + this.writeLength(length); + if (length) { + this.ensure(length); + this._buffer.write(s!, this.offset); + this.offset += length; + } + } + + public writeBuffer = (buffer: Buffer, tag?: number): void => { + if (!isBuffer(buffer)) { + throw new TypeError('Argument must be valid buffer'); + } + if (isNumber(tag)) { + this.writeByte(tag); + this.writeLength(buffer.length); + } + + if (buffer.length > 0) { + this.ensure(buffer.length); + buffer.copy(this._buffer, this.offset, 0, buffer.length); + this.offset += buffer.length; + } + } + + public writeStringArray = (string: string[], tag?: number): void => { + if (!(string instanceof Array)) { + throw new TypeError('Argument must be an Array[String]'); + } + string.forEach((s: string) => this.writeString(s, tag)); + } + + public writeOID = (oid: string, tag?: number): void => { + if (!isString(oid)) { + throw new TypeError(`Argument must be of type string`); + } + + const tagVal = isNumber(tag) ? tag : E_ASN1_TYPES.OID; + + if (!isOID(oid)) { + throw new Error('Argument is not a valid OID string'); + } + + const oidNodes: string[] = oid.split('.'); + + const bytes: number[] = [ + (Number.parseInt(oidNodes[0], 10) * 40 + Number.parseInt(oidNodes[1], 10)), + ]; + + oidNodes.slice(2).forEach((b: string) => encodeOctet(bytes, Number.parseInt(b, 10))); + + this.ensure(2 + bytes.length); + + this.writeByte(tagVal); + + this.writeLength(bytes.length); + + bytes.forEach((v: number) => this.writeByte(v)); + } + + public writeLength = (length: number): void => { + if (!isNumber(length)) { + throw new TypeError(`Length must be a number`); + } + this.ensure(4); + if (length <= 0x7f) { + this._buffer[this.offset++] = length; + } else if (length <= 0xff) { + this._buffer[this.offset++] = 0x81; + this._buffer[this.offset++] = length; + } else if (length <= 0xffff) { + this._buffer[this.offset++] = 0x82; + this._buffer[this.offset++] = length >> 8; + this._buffer[this.offset++] = length; + } else if (length <= 0xffffff) { + this._buffer[this.offset++] = 0x83; + this._buffer[this.offset++] = length >> 16; + this._buffer[this.offset++] = length >> 8; + this._buffer[this.offset++] = length; + } else { + throw new InvalidAsn1Error('Length too long (> 4 bytes)'); + } + } + + public startSequence = (tag?: number): void => { + const tagVal = isNumber(tag) ? tag : E_ASN1_TYPES.SEQUENCE | E_ASN1_TYPES.CONSTRUCTOR; + this.writeByte(tagVal); + this.sequence.push(this.offset); + this.ensure(3); + this.offset += 3; + } + + public endSequence = (): void => { + const seq = this.sequence.pop(); + const start = seq! + 3; + const length = this.offset - start; + + if (length <= 0x7f) { + this.shift(start, length, -2); + this._buffer[seq!] = length; + } else if (length <= 0xff) { + this.shift(start, length, -1); + this._buffer[seq!] = 0x81; + this._buffer[seq! + 1] = length; + } else if (length <= 0xffff) { + this._buffer[seq!] = 0x82; + this._buffer[seq! + 1] = length >> 8; + this._buffer[seq! + 2] = length; + } else if (length <= 0xffffff) { + this.shift(start, length, 1); + this._buffer[seq!] = 0x83; + this._buffer[seq! + 1] = length >> 16; + this._buffer[seq! + 2] = length >> 8; + this._buffer[seq! + 3] = length; + } else { + throw new InvalidAsn1Error('Sequence too long'); + } + } + +} \ No newline at end of file diff --git a/src/common/asn1.types.ts b/src/common/asn1.types.ts new file mode 100644 index 0000000..b943ea3 --- /dev/null +++ b/src/common/asn1.types.ts @@ -0,0 +1,78 @@ +/** + * -- A List of ASN1 Data Types -- + * Any value with out-of-order OR new order is marked with a note comment + * @WARN: Before chaning or re-ordering, keep the values in mind + */ +export enum E_ASN1_TYPES { + EOC = 0, + BOOLEAN, + INTEGER, + BIT_STRING, + OCTET_STRING, + NULL, + OID, + OBJECT_DESCRIPTOR, + EXTERNAL, + REAL, + ENUMERATION, + PDV, + UTF8_STRING, + RELATIVE_OID, + // @NOTE: Order change + SEQUENCE = 16, + SET, + NUMERIC_STRING, + PRINTABLE_STRING, + T61_STRING, + VIDEO_TEXT_STRING, + IA5_STRING, + UTC_TIME, + GENERALIZED_TIME, + GRAPHIC_STRING, + VISIBLE_STRING, + // @NOTE: Order change + GENERAL_STRING = 28, + UNIVERSAL_STRING, + CHARATER_STRING, + BMP_STRING, + CONSTRUCTOR, + // @NOTE: order change + CONTEXT = 128, + +} + +export type BER_ASN1_TYPES = {[key: string]: E_ASN1_TYPES }; + +export const BER_ASN1_TYPES: BER_ASN1_TYPES = { + 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, +} \ No newline at end of file diff --git a/src/common/errors.ts b/src/common/errors.ts new file mode 100644 index 0000000..5c56837 --- /dev/null +++ b/src/common/errors.ts @@ -0,0 +1,6 @@ +export class InvalidAsn1Error extends Error { + public readonly name: string = InvalidAsn1Error.name; + constructor(message: string) { + super(message) + } +} \ No newline at end of file diff --git a/src/common/util.ts b/src/common/util.ts new file mode 100644 index 0000000..f494c7f --- /dev/null +++ b/src/common/util.ts @@ -0,0 +1,40 @@ +/** + * Merges properties from one object to another + * @WARN: will modify the `to` parameter in-place + * @param {T} from - source object to inherit from + * @param {T} to - destination object to embed properties into + * @returns {T} - modified `to` parameter + */ +export const mergeObject = (from: T, to: T): T => { + const keys = Object.getOwnPropertyNames(from); + keys.forEach((key: string) => { + if (Object.prototype.hasOwnProperty.call(to, key)) return; + const val = Object.getOwnPropertyDescriptor(from, key); + Object.defineProperty(to, key, val!); + }); + return to; +} + +export const 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); + } +} \ No newline at end of file diff --git a/src/common/validation.ts b/src/common/validation.ts new file mode 100644 index 0000000..842910d --- /dev/null +++ b/src/common/validation.ts @@ -0,0 +1,23 @@ +export const isNil = (data: T): boolean => { + return data === null || data === undefined; +}; + +export const isBuffer = (data: T): boolean => { + return !isNil(data) && Buffer.isBuffer(data); +}; + +export const isNumber = (data: T): boolean => { + return typeof data === 'number' && Number.isFinite(data); +}; + +export const isBoolean = (data: T): boolean => { + return typeof data === 'boolean'; +} + +export const isString = (data: T): boolean => { + return typeof data === 'string'; +} + +export const isOID = (data: string): boolean => { + return /^([0-9]+\.){0,}[0-9]+$/.test(data); +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..116f897 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,11 @@ +import Reader from "./ber/reader"; +import Writer from "./ber/writer"; +import { InvalidAsn1Error } from "./common/errors"; +import { BER_ASN1_TYPES } from "./common/asn1.types"; + +module.exports = { + ...BER_ASN1_TYPES, + BerReader: Reader, + BerWriter: Writer, + InvalidAsn1Error, +}; diff --git a/test/unittests_lib-ber-reader.js b/test/unittests_lib-ber-reader.js index 93aa115..7a6221c 100644 --- a/test/unittests_lib-ber-reader.js +++ b/test/unittests_lib-ber-reader.js @@ -1,6 +1,6 @@ -var asn1 = require("../") -var assert = require("assert") +var asn1 = require("../lib") +var assert = require("chai").assert; var BerReader = asn1.BerReader diff --git a/test/unittests_lib-ber-writer.js b/test/unittests_lib-ber-writer.js index 2b09c09..faef064 100644 --- a/test/unittests_lib-ber-writer.js +++ b/test/unittests_lib-ber-writer.js @@ -1,6 +1,6 @@ -var asn1 = require("../") -var assert = require("assert") +var asn1 = require("../lib") +var assert = require("chai").assert; var BerWriter = asn1.BerWriter diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d4b2dec --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "es6", + "lib": ["es2015"], + "module": "commonjs", + "rootDir": "./src", + "declaration": true, + + "outDir": "./lib/", /* Specify an output folder for all emitted files. */ + "removeComments": true, + "newLine": "lf", + "stripInternal": true, + "noEmitOnError": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "alwaysStrict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowUnreachableCode": true, + "skipLibCheck": true, + }, + "include": ["./src/**/*.ts"], + "exclude": ["./src/**/*.spec.ts"] +}