'use strict' const types = require('./types') const bufferToHexDump = require('../buffer-to-hex-dump') /** * Given a buffer of ASN.1 data encoded according to Basic Encoding Rules (BER), * the reader provides methods for iterating that data and decoding it into * regular JavaScript types. */ class BerReader { /** * 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 /** * An ASN.1 field consists of a tag, a length, and a value. This property * records the length of the current field. * * @type {number} */ #currentFieldLength = 0 /** * Records the offset in the buffer where the most recent {@link readSequence} * was invoked. This is used to facilitate slicing of whole sequences from * the buffer as a new {@link BerReader} instance. * * @type {number} */ #currentSequenceStart = 0 /** * As the BER buffer is read, this property records the current position * in the buffer. * * @type {number} */ #offset = 0 /** * @param {Buffer} buffer */ constructor (buffer) { if (Buffer.isBuffer(buffer) === false) { throw TypeError('Must supply a Buffer instance to read.') } this.#buffer = buffer.subarray(0) this.#size = this.#buffer.length } get [Symbol.toStringTag] () { return 'BerReader' } /** * Get a buffer that represents the underlying data buffer. * * @type {Buffer} */ get buffer () { return this.#buffer.subarray(0) } /** * The length of the current field being read. * * @type {number} */ get length () { return this.#currentFieldLength } /** * Current read position in the underlying data buffer. * * @type {number} */ get offset () { return this.#offset } /** * The number of bytes remaining in the backing buffer that have not * been read. * * @type {number} */ get remain () { return this.#size - this.#offset } /** * Read the next byte in the buffer without advancing the offset. * * @return {number | null} The next byte or null if not enough data. */ peek () { return this.readByte(true) } /** * Reads a boolean from the current offset and advances the offset. * * @param {number} [tag] The tag number that is expected to be read. * * @returns {boolean} True if the tag value represents `true`, otherwise * `false`. * * @throws When there is an error reading the tag. */ readBoolean (tag = types.Boolean) { const intBuffer = this.readTag(tag) this.#offset += intBuffer.length const int = parseIntegerBuffer(intBuffer) return (int !== 0) } /** * 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=false] `true` means don't move the offset. * @returns {number | null} The next byte, `null` if not enough data. */ readByte (peek = false) { if (this.#size - this.#offset < 1) { return null } const byte = this.#buffer[this.#offset] & 0xff if (peek !== true) { this.#offset += 1 } return byte } /** * Reads an enumeration (integer) from the current offset and advances the * offset. * * @returns {number} The integer represented by the next sequence of bytes * in the buffer from the current offset. The current offset must be at a * byte whose value is equal to the ASN.1 enumeration tag. * * @throws When there is an error reading the tag. */ readEnumeration () { const intBuffer = this.readTag(types.Enumeration) this.#offset += intBuffer.length return parseIntegerBuffer(intBuffer) } /** * Reads an integer from the current offset and advances the offset. * * @param {number} [tag] The tag number that is expected to be read. * * @returns {number} The integer represented by the next sequence of bytes * in the buffer from the current offset. The current offset must be at a * byte whose value is equal to the ASN.1 integer tag. * * @throws When there is an error reading the tag. */ readInt (tag = types.Integer) { const intBuffer = this.readTag(tag) this.#offset += intBuffer.length return parseIntegerBuffer(intBuffer) } /** * Reads a length value from the BER buffer at the given offset. This * method is not really meant to be called directly, as callers have to * manipulate the internal buffer afterwards. * * This method does not advance the reader offset. * * As a result of this method, the `.length` property can be read for the * current field until another method invokes `readLength`. * * Note: we only support up to 4 bytes to describe the length of a value. * * @param {number} [offset] Read a length value starting at the specified * position in the underlying buffer. * * @return {number | null} The position the buffer should be advanced to in * order for the reader to be at the start of the value for the field. See * {@link setOffset}. If the offset, or length, exceeds the size of the * underlying buffer, `null` will be returned. * * @throws When an unsupported length value is encountered. */ readLength (offset) { if (offset === undefined) { offset = this.#offset } if (offset >= this.#size) { return null } let lengthByte = this.#buffer[offset++] & 0xff // TODO: we are commenting this out because it seems to be unreachable. // It is not clear to me how we can ever check `lenB === null` as `null` // is a primitive type, and seemingly cannot be represented by a byte. // If we find that removal of this line does not affect the larger suite // of ldapjs tests, we should just completely remove it from the code. /* if (lenB === null) { return null } */ if ((lengthByte & 0x80) === 0x80) { lengthByte &= 0x7f // https://www.rfc-editor.org/rfc/rfc4511.html#section-5.1 prohibits // indefinite form (0x80). if (lengthByte === 0) { throw Error('Indefinite length not supported.') } // We only support up to 4 bytes to describe encoding length. So the only // valid indicators are 0x81, 0x82, 0x83, and 0x84. if (lengthByte > 4) { throw Error('Encoding too long.') } if (this.#size - offset < lengthByte) { return null } this.#currentFieldLength = 0 for (let i = 0; i < lengthByte; i++) { this.#currentFieldLength = (this.#currentFieldLength << 8) + (this.#buffer[offset++] & 0xff) } } else { // Wasn't a variable length this.#currentFieldLength = lengthByte } return offset } /** * At the current offset, read the next tag, length, and value as an * object identifier (OID) and return the OID string. * * @param {number} [tag] The tag number that is expected to be read. * * @returns {string | null} Will return `null` if the buffer is an invalid * length. Otherwise, returns the OID as a string. */ readOID (tag = types.OID) { // See https://web.archive.org/web/20221008202056/https://learn.microsoft.com/en-us/windows/win32/seccertenroll/about-object-identifier?redirectedfrom=MSDN const oidBuffer = this.readString(tag, true) if (oidBuffer === null) { return null } const values = [] let value = 0 for (let i = 0; i < oidBuffer.length; i++) { const byte = oidBuffer[i] & 0xff value <<= 7 value += byte & 0x7f if ((byte & 0x80) === 0) { values.push(value) value = 0 } } value = values.shift() values.unshift(value % 40) values.unshift((value / 40) >> 0) return values.join('.') } /** * Get a new {@link Buffer} instance that represents the full set of bytes * for a BER representation of a specified tag. For example, this is useful * when construction objects from an incoming LDAP message and the object * constructor can read a BER representation of itself to create a new * instance, e.g. when reading the filter section of a "search request" * message. * * @param {number} tag The expected tag that starts the TLV series of bytes. * @param {boolean} [advanceOffset=true] Indicates if the instance's internal * offset should be advanced or not after reading the buffer. * * @returns {Buffer|null} If there is a problem reading the buffer, e.g. * the number of bytes indicated by the length do not exist in the value, then * `null` will be returned. Otherwise, a new {@link Buffer} of bytes that * represents a full TLV. */ readRawBuffer (tag, advanceOffset = true) { if (Number.isInteger(tag) === false) { throw Error('must specify an integer tag') } const foundTag = this.peek() if (foundTag !== tag) { const expected = tag.toString(16).padStart(2, '0') const found = foundTag.toString(16).padStart(2, '0') throw Error(`Expected 0x${expected}: got 0x${found}`) } const currentOffset = this.#offset const valueOffset = this.readLength(currentOffset + 1) if (valueOffset === null) { return null } const valueBytesLength = this.length const numTagAndLengthBytes = valueOffset - currentOffset // Buffer.subarray is not inclusive. We need to account for the // tag and length bytes. const endPos = currentOffset + valueBytesLength + numTagAndLengthBytes if (endPos > this.buffer.byteLength) { return null } const buffer = this.buffer.subarray(currentOffset, endPos) if (advanceOffset === true) { this.setOffset(currentOffset + (valueBytesLength + numTagAndLengthBytes)) } return buffer } /** * At the current buffer offset, read the next tag as a sequence tag, and * advance the offset to the position of the tag of the first item in the * sequence. * * @param {number} [tag] The tag number that is expected to be read. * * @returns {number|null} The read sequence tag value. Should match the * function input parameter value. * * @throws If the `tag` does not match or if there is an error reading * the length of the sequence. */ readSequence (tag) { const foundTag = this.peek() if (tag !== undefined && tag !== foundTag) { const expected = tag.toString(16).padStart(2, '0') const found = foundTag.toString(16).padStart(2, '0') throw Error(`Expected 0x${expected}: got 0x${found}`) } this.#currentSequenceStart = this.#offset const valueOffset = this.readLength(this.#offset + 1) // stored in `length` if (valueOffset === null) { return null } this.#offset = valueOffset return foundTag } /** * At the current buffer offset, read the next value as a string and advance * the offset. * * @param {number} [tag] The tag number that is expected to be read. Should * be `ASN1.String`. * @param {boolean} [asBuffer=false] When true, the raw buffer will be * returned. Otherwise, a native string. * * @returns {string | Buffer | null} Will return `null` if the buffer is * malformed. * * @throws If there is a problem reading the length. */ readString (tag = types.OctetString, asBuffer = false) { const tagByte = this.peek() if (tagByte !== tag) { const expected = tag.toString(16).padStart(2, '0') const found = tagByte.toString(16).padStart(2, '0') throw Error(`Expected 0x${expected}: got 0x${found}`) } const valueOffset = this.readLength(this.#offset + 1) // stored in `length` if (valueOffset === null) { return null } if (this.length > this.#size - valueOffset) { return null } this.#offset = valueOffset if (this.length === 0) { return asBuffer ? Buffer.alloc(0) : '' } const str = this.#buffer.subarray(this.#offset, this.#offset + this.length) this.#offset += this.length return asBuffer ? str : str.toString('utf8') } /** * At the current buffer offset, read the next set of bytes represented * by the given tag, and return the resulting buffer. For example, if the * BER represents a sequence with a string "foo", i.e. * `[0x30, 0x05, 0x04, 0x03, 0x66, 0x6f, 0x6f]`, and the current offset is * `0`, then the result of `readTag(0x30)` is the buffer * `[0x04, 0x03, 0x66, 0x6f, 0x6f]`. * * @param {number} tag The tag number that is expected to be read. * * @returns {Buffer | null} The buffer representing the tag value, or null if * the buffer is in some way malformed. * * @throws When there is an error interpreting the buffer, or the buffer * is not formed correctly. */ readTag (tag) { if (tag == null) { throw Error('Must supply an ASN.1 tag to read.') } const byte = this.peek() if (byte !== tag) { const tagString = tag.toString(16).padStart(2, '0') const byteString = byte.toString(16).padStart(2, '0') throw Error(`Expected 0x${tagString}: got 0x${byteString}`) } const fieldOffset = this.readLength(this.#offset + 1) // stored in `length` if (fieldOffset === null) { return null } if (this.length > this.#size - fieldOffset) { return null } this.#offset = fieldOffset return this.#buffer.subarray(this.#offset, this.#offset + this.length) } /** * Returns the current sequence as a new {@link BerReader} instance. This * method relies on {@link readSequence} having been invoked first. If it has * not been invoked, the returned reader will represent an undefined portion * of the underlying buffer. * * @returns {BerReader} */ sequenceToReader () { // Represents the number of bytes that constitute the "length" portion // of the TLV tuple. const lengthValueLength = this.#offset - this.#currentSequenceStart const buffer = this.#buffer.subarray( this.#currentSequenceStart, this.#currentSequenceStart + (lengthValueLength + this.#currentFieldLength) ) return new BerReader(buffer) } /** * Set the internal offset to a given position in the underlying buffer. * This method is to support manual advancement of the reader. * * @param {number} position * * @throws If the given `position` is not an integer. */ setOffset (position) { if (Number.isInteger(position) === false) { throw Error('Must supply an integer position.') } this.#offset = position } /** * @param {HexDumpParams} params The `buffer` parameter will be ignored. * * @see bufferToHexDump */ toHexDump (params) { bufferToHexDump({ ...params, buffer: this.buffer }) } } /** * Given a buffer that represents an integer TLV, parse it and return it * as a decimal value. This accounts for signedness. * * @param {Buffer} integerBuffer * * @returns {number} */ function parseIntegerBuffer (integerBuffer) { let value = 0 let i for (i = 0; i < integerBuffer.length; i++) { value <<= 8 value |= (integerBuffer[i] & 0xff) } if ((integerBuffer[0] & 0x80) === 0x80 && i !== 4) { value -= (1 << (i * 8)) } return value >> 0 } module.exports = BerReader