wangzhibo
2026-05-09 53418655c061ac26f66058253f39200aced1ab1d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
'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