Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
index.js551 linesDownload Raw Back to lib
1'use strict'
2
3const crypto = require('crypto')
4const { Minipass } = require('minipass')
5
6const SPEC_ALGORITHMS = ['sha512', 'sha384', 'sha256']
7const DEFAULT_ALGORITHMS = ['sha512']
8const NODE_HASHES = crypto.getHashes()
9
10// TODO: this should really be a hardcoded list of algorithms we support, rather than [a-z0-9].
11const BASE64_REGEX = /^[a-z0-9+/]+(?:=?=?)$/i
12const SRI_REGEX = /^([a-z0-9]+)-([^?]+)(\?[?\S*]*)?$/
13const STRICT_SRI_REGEX = /^([a-z0-9]+)-([A-Za-z0-9+/=]{44,88})(\?[\x21-\x7E]*)?$/
14const VCHAR_REGEX = /^[\x21-\x7E]+$/
15
16// This is a Best Effort™ at a reasonable priority for hash algos
17const DEFAULT_PRIORITY = [
18  'md5', 'whirlpool', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512',
19  // TODO - it's unclear _which_ of these Node will actually use as its name for the algorithm, so we guesswork it based on the OpenSSL names.
20  'sha3', 'sha3-256', 'sha3-384', 'sha3-512', 'sha3_256', 'sha3_384', 'sha3_512',
21].filter(algo => NODE_HASHES.includes(algo))
22
23const getOptString = options => options?.length ? `?${options.join('?')}` : ''
24
25class IntegrityStream extends Minipass {
26  #emittedIntegrity
27  #emittedSize
28  #emittedVerified
29
30  constructor (opts) {
31    super()
32    this.size = 0
33    this.opts = opts
34
35    // may be overridden later, but set now for class consistency
36    this.#getOptions()
37
38    // options used for calculating stream.  can't be changed.
39    if (opts?.algorithms) {
40      this.algorithms = [...opts.algorithms]
41    } else {
42      this.algorithms = [...DEFAULT_ALGORITHMS]
43    }
44    if (this.algorithm !== null && !this.algorithms.includes(this.algorithm)) {
45      this.algorithms.push(this.algorithm)
46    }
47
48    this.hashes = this.algorithms.map(crypto.createHash)
49  }
50
51  #getOptions () {
52    // For verification
53    this.sri = this.opts?.integrity ? parse(this.opts?.integrity, this.opts) : null
54    this.expectedSize = this.opts?.size
55
56    if (!this.sri) {
57      this.algorithm = null
58    } else if (this.sri.isHash) {
59      this.goodSri = true
60      this.algorithm = this.sri.algorithm
61    } else {
62      this.goodSri = !this.sri.isEmpty()
63      this.algorithm = this.sri.pickAlgorithm(this.opts)
64    }
65
66    this.digests = this.goodSri ? this.sri[this.algorithm] : null
67    this.optString = getOptString(this.opts?.options)
68  }
69
70  on (ev, handler) {
71    if (ev === 'size' && this.#emittedSize) {
72      return handler(this.#emittedSize)
73    }
74
75    if (ev === 'integrity' && this.#emittedIntegrity) {
76      return handler(this.#emittedIntegrity)
77    }
78
79    if (ev === 'verified' && this.#emittedVerified) {
80      return handler(this.#emittedVerified)
81    }
82
83    return super.on(ev, handler)
84  }
85
86  emit (ev, data) {
87    if (ev === 'end') {
88      this.#onEnd()
89    }
90    return super.emit(ev, data)
91  }
92
93  write (data) {
94    this.size += data.length
95    this.hashes.forEach(h => h.update(data))
96    return super.write(data)
97  }
98
99  #onEnd () {
100    if (!this.goodSri) {
101      this.#getOptions()
102    }
103    const newSri = parse(this.hashes.map((h, i) => {
104      return `${this.algorithms[i]}-${h.digest('base64')}${this.optString}`
105    }).join(' '), this.opts)
106    // Integrity verification mode
107    const match = this.goodSri && newSri.match(this.sri, this.opts)
108    if (typeof this.expectedSize === 'number' && this.size !== this.expectedSize) {
109      const err = new Error(`stream size mismatch when checking ${this.sri}.\n  Wanted: ${this.expectedSize}\n  Found: ${this.size}`)
110      err.code = 'EBADSIZE'
111      err.found = this.size
112      err.expected = this.expectedSize
113      err.sri = this.sri
114      this.emit('error', err)
115    } else if (this.sri && !match) {
116      const err = new Error(`${this.sri} integrity checksum failed when using ${this.algorithm}: wanted ${this.digests} but got ${newSri}. (${this.size} bytes)`)
117      err.code = 'EINTEGRITY'
118      err.found = newSri
119      err.expected = this.digests
120      err.algorithm = this.algorithm
121      err.sri = this.sri
122      this.emit('error', err)
123    } else {
124      this.#emittedSize = this.size
125      this.emit('size', this.size)
126      this.#emittedIntegrity = newSri
127      this.emit('integrity', newSri)
128      if (match) {
129        this.#emittedVerified = match
130        this.emit('verified', match)
131      }
132    }
133  }
134}
135
136class Hash {
137  get isHash () {
138    return true
139  }
140
141  constructor (hash, opts) {
142    const strict = opts?.strict
143    this.source = hash.trim()
144
145    // set default values so that we make V8 happy to always see a familiar object template.
146    this.digest = ''
147    this.algorithm = ''
148    this.options = []
149
150    // 3.1. Integrity metadata (called "Hash" by ssri)
151    // https://w3c.github.io/webappsec-subresource-integrity/#integrity-metadata-description
152    const match = this.source.match(
153      strict
154        ? STRICT_SRI_REGEX
155        : SRI_REGEX
156    )
157    if (!match) {
158      return
159    }
160    if (strict && !SPEC_ALGORITHMS.includes(match[1])) {
161      return
162    }
163    if (!NODE_HASHES.includes(match[1])) {
164      return
165    }
166    this.algorithm = match[1]
167    this.digest = match[2]
168
169    const rawOpts = match[3]
170    if (rawOpts) {
171      this.options = rawOpts.slice(1).split('?')
172    }
173  }
174
175  hexDigest () {
176    return this.digest && Buffer.from(this.digest, 'base64').toString('hex')
177  }
178
179  toJSON () {
180    return this.toString()
181  }
182
183  match (integrity, opts) {
184    const other = parse(integrity, opts)
185    if (!other) {
186      return false
187    }
188    if (other.isIntegrity) {
189      const algo = other.pickAlgorithm(opts, [this.algorithm])
190
191      if (!algo) {
192        return false
193      }
194
195      const foundHash = other[algo].find(hash => hash.digest === this.digest)
196
197      if (foundHash) {
198        return foundHash
199      }
200
201      return false
202    }
203    return other.digest === this.digest ? other : false
204  }
205
206  toString (opts) {
207    if (opts?.strict) {
208      // Strict mode enforces the standard as close to the foot of the letter as it can.
209      if (!(
210        // The spec has very restricted productions for algorithms.
211        // https://www.w3.org/TR/CSP2/#source-list-syntax
212        SPEC_ALGORITHMS.includes(this.algorithm) &&
213        // Usually, if someone insists on using a "different" base64, we leave it as-is, since there are multiple standards, and the specified is not a URL-safe variant.
214        // https://www.w3.org/TR/CSP2/#base64_value
215        this.digest.match(BASE64_REGEX) &&
216        // Option syntax is strictly visual chars.
217        // https://w3c.github.io/webappsec-subresource-integrity/#grammardef-option-expression
218        // https://tools.ietf.org/html/rfc5234#appendix-B.1
219        this.options.every(opt => opt.match(VCHAR_REGEX))
220      )) {
221        return ''
222      }
223    }
224    return `${this.algorithm}-${this.digest}${getOptString(this.options)}`
225  }
226}
227
228function integrityHashToString (toString, sep, opts, hashes) {
229  const toStringIsNotEmpty = toString !== ''
230
231  let shouldAddFirstSep = false
232  let complement = ''
233
234  const lastIndex = hashes.length - 1
235
236  for (let i = 0; i < lastIndex; i++) {
237    const hashString = Hash.prototype.toString.call(hashes[i], opts)
238
239    if (hashString) {
240      shouldAddFirstSep = true
241
242      complement += hashString
243      complement += sep
244    }
245  }
246
247  const finalHashString = Hash.prototype.toString.call(hashes[lastIndex], opts)
248
249  if (finalHashString) {
250    shouldAddFirstSep = true
251    complement += finalHashString
252  }
253
254  if (toStringIsNotEmpty && shouldAddFirstSep) {
255    return toString + sep + complement
256  }
257
258  return toString + complement
259}
260
261class Integrity {
262  get isIntegrity () {
263    return true
264  }
265
266  toJSON () {
267    return this.toString()
268  }
269
270  isEmpty () {
271    return Object.keys(this).length === 0
272  }
273
274  toString (opts) {
275    let sep = opts?.sep || ' '
276    let toString = ''
277
278    if (opts?.strict) {
279      // Entries must be separated by whitespace, according to spec.
280      sep = sep.replace(/\S+/g, ' ')
281
282      for (const hash of SPEC_ALGORITHMS) {
283        if (this[hash]) {
284          toString = integrityHashToString(toString, sep, opts, this[hash])
285        }
286      }
287    } else {
288      for (const hash of Object.keys(this)) {
289        toString = integrityHashToString(toString, sep, opts, this[hash])
290      }
291    }
292
293    return toString
294  }
295
296  concat (integrity, opts) {
297    const other = typeof integrity === 'string'
298      ? integrity
299      : stringify(integrity, opts)
300    return parse(`${this.toString(opts)} ${other}`, opts)
301  }
302
303  hexDigest () {
304    return parse(this, { single: true }).hexDigest()
305  }
306
307  // add additional hashes to an integrity value, but prevent *changing* an existing integrity hash.
308  merge (integrity, opts) {
309    const other = parse(integrity, opts)
310    for (const algo in other) {
311      if (this[algo]) {
312        if (!this[algo].find(hash =>
313          other[algo].find(otherhash =>
314            hash.digest === otherhash.digest))) {
315          throw new Error('hashes do not match, cannot update integrity')
316        }
317      } else {
318        this[algo] = other[algo]
319      }
320    }
321  }
322
323  match (integrity, opts) {
324    const other = parse(integrity, opts)
325    if (!other) {
326      return false
327    }
328    const algo = other.pickAlgorithm(opts, Object.keys(this))
329    return !!algo && this[algo].find(hash =>
330      other[algo].find(otherhash =>
331        hash.digest === otherhash.digest
332      )
333    ) || false
334  }
335
336  // Pick the highest priority algorithm present, optionally also limited to a set of hashes found in another integrity.
337  // When limiting it may return nothing.
338  pickAlgorithm (opts, hashes) {
339    const pickAlgorithm = opts?.pickAlgorithm || getPrioritizedHash
340    let keys = Object.keys(this)
341    if (hashes?.length) {
342      keys = keys.filter(k => hashes.includes(k))
343    }
344    if (keys.length) {
345      return keys.reduce((acc, algo) => pickAlgorithm(acc, algo) || acc)
346    }
347    // no intersection between this and hashes
348    return null
349  }
350}
351
352module.exports.parse = parse
353function parse (sri, opts) {
354  if (!sri) {
355    return null
356  }
357  if (typeof sri === 'string') {
358    return _parse(sri, opts)
359  } else if (sri.algorithm && sri.digest) {
360    const fullSri = new Integrity()
361    fullSri[sri.algorithm] = [sri]
362    return _parse(stringify(fullSri, opts), opts)
363  } else {
364    return _parse(stringify(sri, opts), opts)
365  }
366}
367
368function _parse (integrity, opts) {
369  // 3.4.3. Parse metadata
370  // https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata
371  if (opts?.single) {
372    return new Hash(integrity, opts)
373  }
374  const hashes = integrity.trim().split(/\s+/).reduce((acc, string) => {
375    const hash = new Hash(string, opts)
376    if (hash.algorithm && hash.digest) {
377      const algo = hash.algorithm
378      if (!Object.keys(acc).includes(algo)) {
379        acc[algo] = []
380      }
381      acc[algo].push(hash)
382    }
383    return acc
384  }, new Integrity())
385  return hashes.isEmpty() ? null : hashes
386}
387
388module.exports.stringify = stringify
389function stringify (obj, opts) {
390  if (obj.algorithm && obj.digest) {
391    return Hash.prototype.toString.call(obj, opts)
392  } else if (typeof obj === 'string') {
393    return stringify(parse(obj, opts), opts)
394  } else {
395    return Integrity.prototype.toString.call(obj, opts)
396  }
397}
398
399module.exports.fromHex = fromHex
400function fromHex (hexDigest, algorithm, opts) {
401  const optString = getOptString(opts?.options)
402  return parse(
403    `${algorithm}-${
404      Buffer.from(hexDigest, 'hex').toString('base64')
405    }${optString}`, opts
406  )
407}
408
409module.exports.fromData = fromData
410function fromData (data, opts) {
411  const algorithms = opts?.algorithms || [...DEFAULT_ALGORITHMS]
412  const optString = getOptString(opts?.options)
413  return algorithms.reduce((acc, algo) => {
414    const digest = crypto.createHash(algo).update(data).digest('base64')
415    const hash = new Hash(
416      `${algo}-${digest}${optString}`,
417      opts
418    )
419    // istanbul ignore else - it would be VERY strange if the string we just calculated with an algo did not have an algo or digest.
420    if (hash.algorithm && hash.digest) {
421      const hashAlgo = hash.algorithm
422      if (!acc[hashAlgo]) {
423        acc[hashAlgo] = []
424      }
425      acc[hashAlgo].push(hash)
426    }
427    return acc
428  }, new Integrity())
429}
430
431module.exports.fromStream = fromStream
432function fromStream (stream, opts) {
433  const istream = integrityStream(opts)
434  return new Promise((resolve, reject) => {
435    stream.pipe(istream)
436    stream.on('error', reject)
437    istream.on('error', reject)
438    let sri
439    istream.on('integrity', s => {
440      sri = s
441    })
442    istream.on('end', () => resolve(sri))
443    istream.resume()
444  })
445}
446
447module.exports.checkData = checkData
448function checkData (data, sri, opts) {
449  sri = parse(sri, opts)
450  if (!sri || !Object.keys(sri).length) {
451    if (opts?.error) {
452      throw Object.assign(
453        new Error('No valid integrity hashes to check against'), {
454          code: 'EINTEGRITY',
455        }
456      )
457    } else {
458      return false
459    }
460  }
461  const algorithm = sri.pickAlgorithm(opts)
462  const digest = crypto.createHash(algorithm).update(data).digest('base64')
463  const newSri = parse({ algorithm, digest })
464  const match = newSri.match(sri, opts)
465  opts = opts || {}
466  if (match || !(opts.error)) {
467    return match
468  } else if (typeof opts.size === 'number' && (data.length !== opts.size)) {
469    const err = new Error(`data size mismatch when checking ${sri}.\n  Wanted: ${opts.size}\n  Found: ${data.length}`)
470    err.code = 'EBADSIZE'
471    err.found = data.length
472    err.expected = opts.size
473    err.sri = sri
474    throw err
475  } else {
476    const err = new Error(`Integrity checksum failed when using ${algorithm}: Wanted ${sri}, but got ${newSri}. (${data.length} bytes)`)
477    err.code = 'EINTEGRITY'
478    err.found = newSri
479    err.expected = sri
480    err.algorithm = algorithm
481    err.sri = sri
482    throw err
483  }
484}
485
486module.exports.checkStream = checkStream
487function checkStream (stream, sri, opts) {
488  opts = opts || Object.create(null)
489  opts.integrity = sri
490  sri = parse(sri, opts)
491  if (!sri || !Object.keys(sri).length) {
492    return Promise.reject(Object.assign(
493      new Error('No valid integrity hashes to check against'), {
494        code: 'EINTEGRITY',
495      }
496    ))
497  }
498  const checker = integrityStream(opts)
499  return new Promise((resolve, reject) => {
500    stream.pipe(checker)
501    stream.on('error', reject)
502    checker.on('error', reject)
503    let verified
504    checker.on('verified', s => {
505      verified = s
506    })
507    checker.on('end', () => resolve(verified))
508    checker.resume()
509  })
510}
511
512module.exports.integrityStream = integrityStream
513function integrityStream (opts = Object.create(null)) {
514  return new IntegrityStream(opts)
515}
516
517module.exports.create = createIntegrity
518function createIntegrity (opts) {
519  const algorithms = opts?.algorithms || [...DEFAULT_ALGORITHMS]
520  const optString = getOptString(opts?.options)
521
522  const hashes = algorithms.map(crypto.createHash)
523
524  return {
525    update: function (chunk, enc) {
526      hashes.forEach(h => h.update(chunk, enc))
527      return this
528    },
529    digest: function () {
530      const integrity = algorithms.reduce((acc, algo) => {
531        const digest = hashes.shift().digest('base64')
532        const hash = new Hash(`${algo}-${digest}${optString}`, opts)
533        if (!acc[hash.algorithm]) {
534          acc[hash.algorithm] = []
535        }
536        acc[hash.algorithm].push(hash)
537        return acc
538      }, new Integrity())
539
540      return integrity
541    },
542  }
543}
544
545function getPrioritizedHash (algo1, algo2) {
546  /* eslint-disable-next-line max-len */
547  return DEFAULT_PRIORITY.indexOf(algo1.toLowerCase()) >= DEFAULT_PRIORITY.indexOf(algo2.toLowerCase())
548    ? algo1
549    : algo2
550}
551 
codekingpro/portable-devtools · Team Ai