// Copyright 2011 Mark Cavage All rights reserved. const assert = require('assert') const ASN1 = require('./types') const errors = require('./errors') // --- Globals const newInvalidAsn1Error = errors.newInvalidAsn1Error const 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') const keys = Object.getOwnPropertyNames(from) keys.forEach(function (key) { if (to[key]) { return } const 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, Symbol.toStringTag, { value: 'BerWriter' }) Object.defineProperty(Writer.prototype, 'buffer', { get: function () { if (this._seq.length) { throw newInvalidAsn1Error(this._seq.length + ' unended sequence(s)') } return (this._buf.slice(0, this._offset)) } }) /** * Append a raw buffer to the current writer instance. No validation to * determine if the buffer represents a valid BER encoding is performed. * * @param {Buffer} buffer The buffer to append. If this is not a valid BER * sequence of data, it will invalidate the BER represented by the `BerWriter`. * * @throws If the input is not an instance of Buffer. */ Writer.prototype.appendBuffer = function appendBuffer (buffer) { if (Buffer.isBuffer(buffer) === false) { throw Error('buffer must be an instance of Buffer') } for (const b of buffer.values()) { this.writeByte(b) } } 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 } let sz = 4 while ((((i & 0xff800000) === 0) || ((i & 0xff800000) === 0xff800000 >> 0)) && (sz > 1)) { sz-- i <<= 8 } if (sz > 4) { throw newInvalidAsn1Error('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 } const 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 (Array.isArray(strings) === false) { throw new TypeError('argument must be an Array[String]') } const self = this strings.forEach(function (s) { self.writeString(s) }) } // 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) } } const tmp = s.split('.') const bytes = [] bytes.push(parseInt(tmp[0], 10) * 40 + parseInt(tmp[1], 10)) tmp.slice(2).forEach(function (b) { encodeOctet(bytes, parseInt(b, 10)) }) const 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 newInvalidAsn1Error('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 () { const seq = this._seq.pop() const start = seq + 3 const 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 newInvalidAsn1Error('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) { let sz = this._size * this._options.growthFactor if (sz - this._offset < len) { sz += len } const buf = Buffer.alloc(sz) this._buf.copy(buf, 0, 0, this._offset) this._buf = buf this._size = sz } } // --- Exported API module.exports = Writer