123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502 |
- '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
|