writer.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. 'use strict'
  2. const types = require('./types')
  3. const bufferToHexDump = require('../buffer-to-hex-dump')
  4. class BerWriter {
  5. /**
  6. * The source buffer as it was passed in when creating the instance.
  7. *
  8. * @type {Buffer}
  9. */
  10. #buffer
  11. /**
  12. * The total bytes in the backing buffer.
  13. *
  14. * @type {number}
  15. */
  16. #size
  17. /**
  18. * As the BER buffer is written, this property records the current position
  19. * in the buffer.
  20. *
  21. * @type {number}
  22. */
  23. #offset = 0
  24. /**
  25. * A list of offsets in the buffer where we need to insert sequence tag and
  26. * length pairs.
  27. */
  28. #sequenceOffsets = []
  29. /**
  30. * Coeffecient used when increasing the buffer to accomodate writes that
  31. * exceed the available space left in the buffer.
  32. *
  33. * @type {number}
  34. */
  35. #growthFactor
  36. constructor ({ size = 1024, growthFactor = 8 } = {}) {
  37. this.#buffer = Buffer.alloc(size)
  38. this.#size = this.#buffer.length
  39. this.#offset = 0
  40. this.#growthFactor = growthFactor
  41. }
  42. get [Symbol.toStringTag] () { return 'BerWriter' }
  43. get buffer () {
  44. // TODO: handle sequence check
  45. return this.#buffer.subarray(0, this.#offset)
  46. }
  47. /**
  48. * The size of the backing buffer.
  49. *
  50. * @return {number}
  51. */
  52. get size () {
  53. return this.#size
  54. }
  55. /**
  56. * Append a raw buffer to the current writer instance. No validation to
  57. * determine if the buffer represents a valid BER encoding is performed.
  58. *
  59. * @param {Buffer} buffer The buffer to append. If this is not a valid BER
  60. * sequence of data, it will invalidate the BER represented by the `BerWriter`.
  61. *
  62. * @throws If the input is not an instance of Buffer.
  63. */
  64. appendBuffer (buffer) {
  65. if (Buffer.isBuffer(buffer) === false) {
  66. throw Error('buffer must be an instance of Buffer')
  67. }
  68. this.#ensureBufferCapacity(buffer.length)
  69. buffer.copy(this.#buffer, this.#offset, 0, buffer.length)
  70. this.#offset += buffer.length
  71. }
  72. /**
  73. * Complete a sequence started with {@link startSequence}.
  74. *
  75. * @throws When the sequence is too long and would exceed the 4 byte
  76. * length descriptor limitation.
  77. */
  78. endSequence () {
  79. const sequenceStartOffset = this.#sequenceOffsets.pop()
  80. const start = sequenceStartOffset + 3
  81. const length = this.#offset - start
  82. if (length <= 0x7f) {
  83. this.#shift(start, length, -2)
  84. this.#buffer[sequenceStartOffset] = length
  85. } else if (length <= 0xff) {
  86. this.#shift(start, length, -1)
  87. this.#buffer[sequenceStartOffset] = 0x81
  88. this.#buffer[sequenceStartOffset + 1] = length
  89. } else if (length <= 0xffff) {
  90. this.#buffer[sequenceStartOffset] = 0x82
  91. this.#buffer[sequenceStartOffset + 1] = length >> 8
  92. this.#buffer[sequenceStartOffset + 2] = length
  93. } else if (length <= 0xffffff) {
  94. this.#shift(start, length, 1)
  95. this.#buffer[sequenceStartOffset] = 0x83
  96. this.#buffer[sequenceStartOffset + 1] = length >> 16
  97. this.#buffer[sequenceStartOffset + 2] = length >> 8
  98. this.#buffer[sequenceStartOffset + 3] = length
  99. } else {
  100. throw Error('sequence too long')
  101. }
  102. }
  103. /**
  104. * Write a sequence tag to the buffer and advance the offset to the starting
  105. * position of the value. Sequences must be completed with a subsequent
  106. * invocation of {@link endSequence}.
  107. *
  108. * @param {number} [tag=0x30] The tag to use for the sequence.
  109. *
  110. * @throws When the tag is not a number.
  111. */
  112. startSequence (tag = (types.Sequence | types.Constructor)) {
  113. if (typeof tag !== 'number') {
  114. throw TypeError('tag must be a Number')
  115. }
  116. this.writeByte(tag)
  117. this.#sequenceOffsets.push(this.#offset)
  118. this.#ensureBufferCapacity(3)
  119. this.#offset += 3
  120. }
  121. /**
  122. * @param {HexDumpParams} params The `buffer` parameter will be ignored.
  123. *
  124. * @see bufferToHexDump
  125. */
  126. toHexDump (params) {
  127. bufferToHexDump({
  128. ...params,
  129. buffer: this.buffer
  130. })
  131. }
  132. /**
  133. * Write a boolean TLV to the buffer.
  134. *
  135. * @param {boolean} boolValue
  136. * @param {tag} [number=0x01] A custom tag for the boolean.
  137. *
  138. * @throws When a parameter is of the wrong type.
  139. */
  140. writeBoolean (boolValue, tag = types.Boolean) {
  141. if (typeof boolValue !== 'boolean') {
  142. throw TypeError('boolValue must be a Boolean')
  143. }
  144. if (typeof tag !== 'number') {
  145. throw TypeError('tag must be a Number')
  146. }
  147. this.#ensureBufferCapacity(3)
  148. this.#buffer[this.#offset++] = tag
  149. this.#buffer[this.#offset++] = 0x01
  150. this.#buffer[this.#offset++] = boolValue === true ? 0xff : 0x00
  151. }
  152. /**
  153. * Write an arbitrary buffer of data to the backing buffer using the given
  154. * tag.
  155. *
  156. * @param {Buffer} buffer
  157. * @param {number} tag The tag to use for the ASN.1 TLV sequence.
  158. *
  159. * @throws When either input parameter is of the wrong type.
  160. */
  161. writeBuffer (buffer, tag) {
  162. if (typeof tag !== 'number') {
  163. throw TypeError('tag must be a Number')
  164. }
  165. if (Buffer.isBuffer(buffer) === false) {
  166. throw TypeError('buffer must be an instance of Buffer')
  167. }
  168. this.writeByte(tag)
  169. this.writeLength(buffer.length)
  170. this.#ensureBufferCapacity(buffer.length)
  171. buffer.copy(this.#buffer, this.#offset, 0, buffer.length)
  172. this.#offset += buffer.length
  173. }
  174. /**
  175. * Write a single byte to the backing buffer and advance the offset. The
  176. * backing buffer will be automatically expanded to accomodate the new byte
  177. * if no room in the buffer remains.
  178. *
  179. * @param {number} byte The byte to be written.
  180. *
  181. * @throws When the passed in parameter is not a `Number` (aka a byte).
  182. */
  183. writeByte (byte) {
  184. if (typeof byte !== 'number') {
  185. throw TypeError('argument must be a Number')
  186. }
  187. this.#ensureBufferCapacity(1)
  188. this.#buffer[this.#offset++] = byte
  189. }
  190. /**
  191. * Write an enumeration TLV to the buffer.
  192. *
  193. * @param {number} value
  194. * @param {number} [tag=0x0a] A custom tag for the enumeration.
  195. *
  196. * @throws When a passed in parameter is not of the correct type, or the
  197. * value requires too many bytes (must be <= 4).
  198. */
  199. writeEnumeration (value, tag = types.Enumeration) {
  200. if (typeof value !== 'number') {
  201. throw TypeError('value must be a Number')
  202. }
  203. if (typeof tag !== 'number') {
  204. throw TypeError('tag must be a Number')
  205. }
  206. this.writeInt(value, tag)
  207. }
  208. /**
  209. * Write an, up to 4 byte, integer TLV to the buffer.
  210. *
  211. * @param {number} intToWrite
  212. * @param {number} [tag=0x02]
  213. *
  214. * @throws When either parameter is not of the write type, or if the
  215. * integer consists of too many bytes.
  216. */
  217. writeInt (intToWrite, tag = types.Integer) {
  218. if (typeof intToWrite !== 'number') {
  219. throw TypeError('intToWrite must be a Number')
  220. }
  221. if (typeof tag !== 'number') {
  222. throw TypeError('tag must be a Number')
  223. }
  224. let intSize = 4
  225. while (
  226. (
  227. ((intToWrite & 0xff800000) === 0) ||
  228. ((intToWrite & 0xff800000) === (0xff800000 >> 0))
  229. ) && (intSize > 1)
  230. ) {
  231. intSize--
  232. intToWrite <<= 8
  233. }
  234. // TODO: figure out how to cover this in a test.
  235. /* istanbul ignore if: needs test */
  236. if (intSize > 4) {
  237. throw Error('BER ints cannot be > 0xffffffff')
  238. }
  239. this.#ensureBufferCapacity(2 + intSize)
  240. this.#buffer[this.#offset++] = tag
  241. this.#buffer[this.#offset++] = intSize
  242. while (intSize-- > 0) {
  243. this.#buffer[this.#offset++] = ((intToWrite & 0xff000000) >>> 24)
  244. intToWrite <<= 8
  245. }
  246. }
  247. /**
  248. * Write a set of length bytes to the backing buffer. Per
  249. * https://www.rfc-editor.org/rfc/rfc4511.html#section-5.1, LDAP message
  250. * BERs prohibit greater than 4 byte lengths. Given we are supporing
  251. * the `ldapjs` module, we limit ourselves to 4 byte lengths.
  252. *
  253. * @param {number} len The length value to write to the buffer.
  254. *
  255. * @throws When the length is not a number or requires too many bytes.
  256. */
  257. writeLength (len) {
  258. if (typeof len !== 'number') {
  259. throw TypeError('argument must be a Number')
  260. }
  261. this.#ensureBufferCapacity(4)
  262. if (len <= 0x7f) {
  263. this.#buffer[this.#offset++] = len
  264. } else if (len <= 0xff) {
  265. this.#buffer[this.#offset++] = 0x81
  266. this.#buffer[this.#offset++] = len
  267. } else if (len <= 0xffff) {
  268. this.#buffer[this.#offset++] = 0x82
  269. this.#buffer[this.#offset++] = len >> 8
  270. this.#buffer[this.#offset++] = len
  271. } else if (len <= 0xffffff) {
  272. this.#buffer[this.#offset++] = 0x83
  273. this.#buffer[this.#offset++] = len >> 16
  274. this.#buffer[this.#offset++] = len >> 8
  275. this.#buffer[this.#offset++] = len
  276. } else {
  277. throw Error('length too long (> 4 bytes)')
  278. }
  279. }
  280. /**
  281. * Write a NULL tag and value to the buffer.
  282. */
  283. writeNull () {
  284. this.writeByte(types.Null)
  285. this.writeByte(0x00)
  286. }
  287. /**
  288. * Given an OID string, e.g. `1.2.840.113549.1.1.1`, split it into
  289. * octets, encode the octets, and write it to the backing buffer.
  290. *
  291. * @param {string} oidString
  292. * @param {number} [tag=0x06] A custom tag to use for the OID.
  293. *
  294. * @throws When the parameters are not of the correct types, or if the
  295. * OID is not in the correct format.
  296. */
  297. writeOID (oidString, tag = types.OID) {
  298. if (typeof oidString !== 'string') {
  299. throw TypeError('oidString must be a string')
  300. }
  301. if (typeof tag !== 'number') {
  302. throw TypeError('tag must be a Number')
  303. }
  304. if (/^([0-9]+\.){3,}[0-9]+$/.test(oidString) === false) {
  305. throw Error('oidString is not a valid OID string')
  306. }
  307. const parts = oidString.split('.')
  308. const bytes = []
  309. bytes.push(parseInt(parts[0], 10) * 40 + parseInt(parts[1], 10))
  310. for (const part of parts.slice(2)) {
  311. encodeOctet(bytes, parseInt(part, 10))
  312. }
  313. this.#ensureBufferCapacity(2 + bytes.length)
  314. this.writeByte(tag)
  315. this.writeLength(bytes.length)
  316. this.appendBuffer(Buffer.from(bytes))
  317. function encodeOctet (bytes, octet) {
  318. if (octet < 128) {
  319. bytes.push(octet)
  320. } else if (octet < 16_384) {
  321. bytes.push((octet >>> 7) | 0x80)
  322. bytes.push(octet & 0x7F)
  323. } else if (octet < 2_097_152) {
  324. bytes.push((octet >>> 14) | 0x80)
  325. bytes.push(((octet >>> 7) | 0x80) & 0xFF)
  326. bytes.push(octet & 0x7F)
  327. } else if (octet < 268_435_456) {
  328. bytes.push((octet >>> 21) | 0x80)
  329. bytes.push(((octet >>> 14) | 0x80) & 0xFF)
  330. bytes.push(((octet >>> 7) | 0x80) & 0xFF)
  331. bytes.push(octet & 0x7F)
  332. } else {
  333. bytes.push(((octet >>> 28) | 0x80) & 0xFF)
  334. bytes.push(((octet >>> 21) | 0x80) & 0xFF)
  335. bytes.push(((octet >>> 14) | 0x80) & 0xFF)
  336. bytes.push(((octet >>> 7) | 0x80) & 0xFF)
  337. bytes.push(octet & 0x7F)
  338. }
  339. }
  340. }
  341. /**
  342. * Write a string TLV to the buffer.
  343. *
  344. * @param {string} stringToWrite
  345. * @param {number} [tag=0x04] The tag to use.
  346. *
  347. * @throws When either input parameter is of the wrong type.
  348. */
  349. writeString (stringToWrite, tag = types.OctetString) {
  350. if (typeof stringToWrite !== 'string') {
  351. throw TypeError('stringToWrite must be a string')
  352. }
  353. if (typeof tag !== 'number') {
  354. throw TypeError('tag must be a number')
  355. }
  356. const toWriteLength = Buffer.byteLength(stringToWrite)
  357. this.writeByte(tag)
  358. this.writeLength(toWriteLength)
  359. if (toWriteLength > 0) {
  360. this.#ensureBufferCapacity(toWriteLength)
  361. this.#buffer.write(stringToWrite, this.#offset)
  362. this.#offset += toWriteLength
  363. }
  364. }
  365. /**
  366. * Given a set of strings, write each as a string TLV to the buffer.
  367. *
  368. * @param {string[]} strings
  369. *
  370. * @throws When the input is not an array.
  371. */
  372. writeStringArray (strings) {
  373. if (Array.isArray(strings) === false) {
  374. throw TypeError('strings must be an instance of Array')
  375. }
  376. for (const string of strings) {
  377. this.writeString(string)
  378. }
  379. }
  380. /**
  381. * Given a number of bytes to be written into the buffer, verify the buffer
  382. * has enough free space. If not, allocate a new buffer, copy the current
  383. * backing buffer into the new buffer, and promote the new buffer to be the
  384. * current backing buffer.
  385. *
  386. * @param {number} numberOfBytesToWrite How many bytes are required to be
  387. * available for writing in the backing buffer.
  388. */
  389. #ensureBufferCapacity (numberOfBytesToWrite) {
  390. if (this.#size - this.#offset < numberOfBytesToWrite) {
  391. let newSize = this.#size * this.#growthFactor
  392. if (newSize - this.#offset < numberOfBytesToWrite) {
  393. newSize += numberOfBytesToWrite
  394. }
  395. const newBuffer = Buffer.alloc(newSize)
  396. this.#buffer.copy(newBuffer, 0, 0, this.#offset)
  397. this.#buffer = newBuffer
  398. this.#size = newSize
  399. }
  400. }
  401. /**
  402. * Shift a region of the buffer indicated by `start` and `length` a number
  403. * of bytes indicated by `shiftAmount`.
  404. *
  405. * @param {number} start The starting position in the buffer for the region
  406. * of bytes to be shifted.
  407. * @param {number} length The number of bytes that constitutes the region
  408. * of the buffer to be shifted.
  409. * @param {number} shiftAmount The number of bytes to shift the region by.
  410. * This may be negative.
  411. */
  412. #shift (start, length, shiftAmount) {
  413. // TODO: this leaves garbage behind. We should either zero out the bytes
  414. // left behind, or device a better algorightm that generates a clean
  415. // buffer.
  416. this.#buffer.copy(this.#buffer, start + shiftAmount, start, start + length)
  417. this.#offset += shiftAmount
  418. }
  419. }
  420. module.exports = BerWriter