Files
ldap-to-oauth2/node_modules/@ldapjs/asn1/lib/ber/writer.js
2025-10-08 11:12:59 -04:00

467 lines
13 KiB
JavaScript

'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