// Copyright 2011 Mark Cavage <mcavage@gmail.com> 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
|