123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466 |
- 'use strict'
- const types = require('./types')
- const bufferToHexDump = require('../buffer-to-hex-dump')
- class BerWriter {
- /**
- * The source buffer as it was passed in when creating the instance.
- *
- * @type {Buffer}
- */
- #buffer
- /**
- * The total bytes in the backing buffer.
- *
- * @type {number}
- */
- #size
- /**
- * As the BER buffer is written, this property records the current position
- * in the buffer.
- *
- * @type {number}
- */
- #offset = 0
- /**
- * A list of offsets in the buffer where we need to insert sequence tag and
- * length pairs.
- */
- #sequenceOffsets = []
- /**
- * Coeffecient used when increasing the buffer to accomodate writes that
- * exceed the available space left in the buffer.
- *
- * @type {number}
- */
- #growthFactor
- constructor ({ size = 1024, growthFactor = 8 } = {}) {
- this.#buffer = Buffer.alloc(size)
- this.#size = this.#buffer.length
- this.#offset = 0
- this.#growthFactor = growthFactor
- }
- get [Symbol.toStringTag] () { return 'BerWriter' }
- get buffer () {
- // TODO: handle sequence check
- return this.#buffer.subarray(0, this.#offset)
- }
- /**
- * The size of the backing buffer.
- *
- * @return {number}
- */
- get size () {
- return this.#size
- }
- /**
- * 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.
- */
- appendBuffer (buffer) {
- if (Buffer.isBuffer(buffer) === false) {
- throw Error('buffer must be an instance of Buffer')
- }
- this.#ensureBufferCapacity(buffer.length)
- buffer.copy(this.#buffer, this.#offset, 0, buffer.length)
- this.#offset += buffer.length
- }
- /**
- * Complete a sequence started with {@link startSequence}.
- *
- * @throws When the sequence is too long and would exceed the 4 byte
- * length descriptor limitation.
- */
- endSequence () {
- const sequenceStartOffset = this.#sequenceOffsets.pop()
- const start = sequenceStartOffset + 3
- const length = this.#offset - start
- if (length <= 0x7f) {
- this.#shift(start, length, -2)
- this.#buffer[sequenceStartOffset] = length
- } else if (length <= 0xff) {
- this.#shift(start, length, -1)
- this.#buffer[sequenceStartOffset] = 0x81
- this.#buffer[sequenceStartOffset + 1] = length
- } else if (length <= 0xffff) {
- this.#buffer[sequenceStartOffset] = 0x82
- this.#buffer[sequenceStartOffset + 1] = length >> 8
- this.#buffer[sequenceStartOffset + 2] = length
- } else if (length <= 0xffffff) {
- this.#shift(start, length, 1)
- this.#buffer[sequenceStartOffset] = 0x83
- this.#buffer[sequenceStartOffset + 1] = length >> 16
- this.#buffer[sequenceStartOffset + 2] = length >> 8
- this.#buffer[sequenceStartOffset + 3] = length
- } else {
- throw Error('sequence too long')
- }
- }
- /**
- * Write a sequence tag to the buffer and advance the offset to the starting
- * position of the value. Sequences must be completed with a subsequent
- * invocation of {@link endSequence}.
- *
- * @param {number} [tag=0x30] The tag to use for the sequence.
- *
- * @throws When the tag is not a number.
- */
- startSequence (tag = (types.Sequence | types.Constructor)) {
- if (typeof tag !== 'number') {
- throw TypeError('tag must be a Number')
- }
- this.writeByte(tag)
- this.#sequenceOffsets.push(this.#offset)
- this.#ensureBufferCapacity(3)
- this.#offset += 3
- }
- /**
- * @param {HexDumpParams} params The `buffer` parameter will be ignored.
- *
- * @see bufferToHexDump
- */
- toHexDump (params) {
- bufferToHexDump({
- ...params,
- buffer: this.buffer
- })
- }
- /**
- * Write a boolean TLV to the buffer.
- *
- * @param {boolean} boolValue
- * @param {tag} [number=0x01] A custom tag for the boolean.
- *
- * @throws When a parameter is of the wrong type.
- */
- writeBoolean (boolValue, tag = types.Boolean) {
- if (typeof boolValue !== 'boolean') {
- throw TypeError('boolValue must be a Boolean')
- }
- if (typeof tag !== 'number') {
- throw TypeError('tag must be a Number')
- }
- this.#ensureBufferCapacity(3)
- this.#buffer[this.#offset++] = tag
- this.#buffer[this.#offset++] = 0x01
- this.#buffer[this.#offset++] = boolValue === true ? 0xff : 0x00
- }
- /**
- * Write an arbitrary buffer of data to the backing buffer using the given
- * tag.
- *
- * @param {Buffer} buffer
- * @param {number} tag The tag to use for the ASN.1 TLV sequence.
- *
- * @throws When either input parameter is of the wrong type.
- */
- writeBuffer (buffer, tag) {
- if (typeof tag !== 'number') {
- throw TypeError('tag must be a Number')
- }
- if (Buffer.isBuffer(buffer) === false) {
- throw TypeError('buffer must be an instance of Buffer')
- }
- this.writeByte(tag)
- this.writeLength(buffer.length)
- this.#ensureBufferCapacity(buffer.length)
- buffer.copy(this.#buffer, this.#offset, 0, buffer.length)
- this.#offset += buffer.length
- }
- /**
- * Write a single byte to the backing buffer and advance the offset. The
- * backing buffer will be automatically expanded to accomodate the new byte
- * if no room in the buffer remains.
- *
- * @param {number} byte The byte to be written.
- *
- * @throws When the passed in parameter is not a `Number` (aka a byte).
- */
- writeByte (byte) {
- if (typeof byte !== 'number') {
- throw TypeError('argument must be a Number')
- }
- this.#ensureBufferCapacity(1)
- this.#buffer[this.#offset++] = byte
- }
- /**
- * Write an enumeration TLV to the buffer.
- *
- * @param {number} value
- * @param {number} [tag=0x0a] A custom tag for the enumeration.
- *
- * @throws When a passed in parameter is not of the correct type, or the
- * value requires too many bytes (must be <= 4).
- */
- writeEnumeration (value, tag = types.Enumeration) {
- if (typeof value !== 'number') {
- throw TypeError('value must be a Number')
- }
- if (typeof tag !== 'number') {
- throw TypeError('tag must be a Number')
- }
- this.writeInt(value, tag)
- }
- /**
- * Write an, up to 4 byte, integer TLV to the buffer.
- *
- * @param {number} intToWrite
- * @param {number} [tag=0x02]
- *
- * @throws When either parameter is not of the write type, or if the
- * integer consists of too many bytes.
- */
- writeInt (intToWrite, tag = types.Integer) {
- if (typeof intToWrite !== 'number') {
- throw TypeError('intToWrite must be a Number')
- }
- if (typeof tag !== 'number') {
- throw TypeError('tag must be a Number')
- }
- let intSize = 4
- while (
- (
- ((intToWrite & 0xff800000) === 0) ||
- ((intToWrite & 0xff800000) === (0xff800000 >> 0))
- ) && (intSize > 1)
- ) {
- intSize--
- intToWrite <<= 8
- }
- // TODO: figure out how to cover this in a test.
- /* istanbul ignore if: needs test */
- if (intSize > 4) {
- throw Error('BER ints cannot be > 0xffffffff')
- }
- this.#ensureBufferCapacity(2 + intSize)
- this.#buffer[this.#offset++] = tag
- this.#buffer[this.#offset++] = intSize
- while (intSize-- > 0) {
- this.#buffer[this.#offset++] = ((intToWrite & 0xff000000) >>> 24)
- intToWrite <<= 8
- }
- }
- /**
- * Write a set of length bytes to the backing buffer. Per
- * https://www.rfc-editor.org/rfc/rfc4511.html#section-5.1, LDAP message
- * BERs prohibit greater than 4 byte lengths. Given we are supporing
- * the `ldapjs` module, we limit ourselves to 4 byte lengths.
- *
- * @param {number} len The length value to write to the buffer.
- *
- * @throws When the length is not a number or requires too many bytes.
- */
- writeLength (len) {
- if (typeof len !== 'number') {
- throw TypeError('argument must be a Number')
- }
- this.#ensureBufferCapacity(4)
- if (len <= 0x7f) {
- this.#buffer[this.#offset++] = len
- } else if (len <= 0xff) {
- this.#buffer[this.#offset++] = 0x81
- this.#buffer[this.#offset++] = len
- } else if (len <= 0xffff) {
- this.#buffer[this.#offset++] = 0x82
- this.#buffer[this.#offset++] = len >> 8
- this.#buffer[this.#offset++] = len
- } else if (len <= 0xffffff) {
- this.#buffer[this.#offset++] = 0x83
- this.#buffer[this.#offset++] = len >> 16
- this.#buffer[this.#offset++] = len >> 8
- this.#buffer[this.#offset++] = len
- } else {
- throw Error('length too long (> 4 bytes)')
- }
- }
- /**
- * Write a NULL tag and value to the buffer.
- */
- writeNull () {
- this.writeByte(types.Null)
- this.writeByte(0x00)
- }
- /**
- * Given an OID string, e.g. `1.2.840.113549.1.1.1`, split it into
- * octets, encode the octets, and write it to the backing buffer.
- *
- * @param {string} oidString
- * @param {number} [tag=0x06] A custom tag to use for the OID.
- *
- * @throws When the parameters are not of the correct types, or if the
- * OID is not in the correct format.
- */
- writeOID (oidString, tag = types.OID) {
- if (typeof oidString !== 'string') {
- throw TypeError('oidString must be a string')
- }
- if (typeof tag !== 'number') {
- throw TypeError('tag must be a Number')
- }
- if (/^([0-9]+\.){3,}[0-9]+$/.test(oidString) === false) {
- throw Error('oidString is not a valid OID string')
- }
- const parts = oidString.split('.')
- const bytes = []
- bytes.push(parseInt(parts[0], 10) * 40 + parseInt(parts[1], 10))
- for (const part of parts.slice(2)) {
- encodeOctet(bytes, parseInt(part, 10))
- }
- this.#ensureBufferCapacity(2 + bytes.length)
- this.writeByte(tag)
- this.writeLength(bytes.length)
- this.appendBuffer(Buffer.from(bytes))
- function encodeOctet (bytes, octet) {
- if (octet < 128) {
- bytes.push(octet)
- } else if (octet < 16_384) {
- bytes.push((octet >>> 7) | 0x80)
- bytes.push(octet & 0x7F)
- } else if (octet < 2_097_152) {
- bytes.push((octet >>> 14) | 0x80)
- bytes.push(((octet >>> 7) | 0x80) & 0xFF)
- bytes.push(octet & 0x7F)
- } else if (octet < 268_435_456) {
- 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)
- }
- }
- }
- /**
- * Write a string TLV to the buffer.
- *
- * @param {string} stringToWrite
- * @param {number} [tag=0x04] The tag to use.
- *
- * @throws When either input parameter is of the wrong type.
- */
- writeString (stringToWrite, tag = types.OctetString) {
- if (typeof stringToWrite !== 'string') {
- throw TypeError('stringToWrite must be a string')
- }
- if (typeof tag !== 'number') {
- throw TypeError('tag must be a number')
- }
- const toWriteLength = Buffer.byteLength(stringToWrite)
- this.writeByte(tag)
- this.writeLength(toWriteLength)
- if (toWriteLength > 0) {
- this.#ensureBufferCapacity(toWriteLength)
- this.#buffer.write(stringToWrite, this.#offset)
- this.#offset += toWriteLength
- }
- }
- /**
- * Given a set of strings, write each as a string TLV to the buffer.
- *
- * @param {string[]} strings
- *
- * @throws When the input is not an array.
- */
- writeStringArray (strings) {
- if (Array.isArray(strings) === false) {
- throw TypeError('strings must be an instance of Array')
- }
- for (const string of strings) {
- this.writeString(string)
- }
- }
- /**
- * Given a number of bytes to be written into the buffer, verify the buffer
- * has enough free space. If not, allocate a new buffer, copy the current
- * backing buffer into the new buffer, and promote the new buffer to be the
- * current backing buffer.
- *
- * @param {number} numberOfBytesToWrite How many bytes are required to be
- * available for writing in the backing buffer.
- */
- #ensureBufferCapacity (numberOfBytesToWrite) {
- if (this.#size - this.#offset < numberOfBytesToWrite) {
- let newSize = this.#size * this.#growthFactor
- if (newSize - this.#offset < numberOfBytesToWrite) {
- newSize += numberOfBytesToWrite
- }
- const newBuffer = Buffer.alloc(newSize)
- this.#buffer.copy(newBuffer, 0, 0, this.#offset)
- this.#buffer = newBuffer
- this.#size = newSize
- }
- }
- /**
- * Shift a region of the buffer indicated by `start` and `length` a number
- * of bytes indicated by `shiftAmount`.
- *
- * @param {number} start The starting position in the buffer for the region
- * of bytes to be shifted.
- * @param {number} length The number of bytes that constitutes the region
- * of the buffer to be shifted.
- * @param {number} shiftAmount The number of bytes to shift the region by.
- * This may be negative.
- */
- #shift (start, length, shiftAmount) {
- // TODO: this leaves garbage behind. We should either zero out the bytes
- // left behind, or device a better algorightm that generates a clean
- // buffer.
- this.#buffer.copy(this.#buffer, start + shiftAmount, start, start + length)
- this.#offset += shiftAmount
- }
- }
- module.exports = BerWriter
|