11cb0ef41Sopenharmony_ci'use strict' 21cb0ef41Sopenharmony_ci 31cb0ef41Sopenharmony_ciconst crypto = require('crypto') 41cb0ef41Sopenharmony_ciconst { Minipass } = require('minipass') 51cb0ef41Sopenharmony_ci 61cb0ef41Sopenharmony_ciconst SPEC_ALGORITHMS = ['sha512', 'sha384', 'sha256'] 71cb0ef41Sopenharmony_ciconst DEFAULT_ALGORITHMS = ['sha512'] 81cb0ef41Sopenharmony_ci 91cb0ef41Sopenharmony_ci// TODO: this should really be a hardcoded list of algorithms we support, 101cb0ef41Sopenharmony_ci// rather than [a-z0-9]. 111cb0ef41Sopenharmony_ciconst BASE64_REGEX = /^[a-z0-9+/]+(?:=?=?)$/i 121cb0ef41Sopenharmony_ciconst SRI_REGEX = /^([a-z0-9]+)-([^?]+)([?\S*]*)$/ 131cb0ef41Sopenharmony_ciconst STRICT_SRI_REGEX = /^([a-z0-9]+)-([A-Za-z0-9+/=]{44,88})(\?[\x21-\x7E]*)?$/ 141cb0ef41Sopenharmony_ciconst VCHAR_REGEX = /^[\x21-\x7E]+$/ 151cb0ef41Sopenharmony_ci 161cb0ef41Sopenharmony_ciconst getOptString = options => options?.length ? `?${options.join('?')}` : '' 171cb0ef41Sopenharmony_ci 181cb0ef41Sopenharmony_ciclass IntegrityStream extends Minipass { 191cb0ef41Sopenharmony_ci #emittedIntegrity 201cb0ef41Sopenharmony_ci #emittedSize 211cb0ef41Sopenharmony_ci #emittedVerified 221cb0ef41Sopenharmony_ci 231cb0ef41Sopenharmony_ci constructor (opts) { 241cb0ef41Sopenharmony_ci super() 251cb0ef41Sopenharmony_ci this.size = 0 261cb0ef41Sopenharmony_ci this.opts = opts 271cb0ef41Sopenharmony_ci 281cb0ef41Sopenharmony_ci // may be overridden later, but set now for class consistency 291cb0ef41Sopenharmony_ci this.#getOptions() 301cb0ef41Sopenharmony_ci 311cb0ef41Sopenharmony_ci // options used for calculating stream. can't be changed. 321cb0ef41Sopenharmony_ci if (opts?.algorithms) { 331cb0ef41Sopenharmony_ci this.algorithms = [...opts.algorithms] 341cb0ef41Sopenharmony_ci } else { 351cb0ef41Sopenharmony_ci this.algorithms = [...DEFAULT_ALGORITHMS] 361cb0ef41Sopenharmony_ci } 371cb0ef41Sopenharmony_ci if (this.algorithm !== null && !this.algorithms.includes(this.algorithm)) { 381cb0ef41Sopenharmony_ci this.algorithms.push(this.algorithm) 391cb0ef41Sopenharmony_ci } 401cb0ef41Sopenharmony_ci 411cb0ef41Sopenharmony_ci this.hashes = this.algorithms.map(crypto.createHash) 421cb0ef41Sopenharmony_ci } 431cb0ef41Sopenharmony_ci 441cb0ef41Sopenharmony_ci #getOptions () { 451cb0ef41Sopenharmony_ci // For verification 461cb0ef41Sopenharmony_ci this.sri = this.opts?.integrity ? parse(this.opts?.integrity, this.opts) : null 471cb0ef41Sopenharmony_ci this.expectedSize = this.opts?.size 481cb0ef41Sopenharmony_ci 491cb0ef41Sopenharmony_ci if (!this.sri) { 501cb0ef41Sopenharmony_ci this.algorithm = null 511cb0ef41Sopenharmony_ci } else if (this.sri.isHash) { 521cb0ef41Sopenharmony_ci this.goodSri = true 531cb0ef41Sopenharmony_ci this.algorithm = this.sri.algorithm 541cb0ef41Sopenharmony_ci } else { 551cb0ef41Sopenharmony_ci this.goodSri = !this.sri.isEmpty() 561cb0ef41Sopenharmony_ci this.algorithm = this.sri.pickAlgorithm(this.opts) 571cb0ef41Sopenharmony_ci } 581cb0ef41Sopenharmony_ci 591cb0ef41Sopenharmony_ci this.digests = this.goodSri ? this.sri[this.algorithm] : null 601cb0ef41Sopenharmony_ci this.optString = getOptString(this.opts?.options) 611cb0ef41Sopenharmony_ci } 621cb0ef41Sopenharmony_ci 631cb0ef41Sopenharmony_ci on (ev, handler) { 641cb0ef41Sopenharmony_ci if (ev === 'size' && this.#emittedSize) { 651cb0ef41Sopenharmony_ci return handler(this.#emittedSize) 661cb0ef41Sopenharmony_ci } 671cb0ef41Sopenharmony_ci 681cb0ef41Sopenharmony_ci if (ev === 'integrity' && this.#emittedIntegrity) { 691cb0ef41Sopenharmony_ci return handler(this.#emittedIntegrity) 701cb0ef41Sopenharmony_ci } 711cb0ef41Sopenharmony_ci 721cb0ef41Sopenharmony_ci if (ev === 'verified' && this.#emittedVerified) { 731cb0ef41Sopenharmony_ci return handler(this.#emittedVerified) 741cb0ef41Sopenharmony_ci } 751cb0ef41Sopenharmony_ci 761cb0ef41Sopenharmony_ci return super.on(ev, handler) 771cb0ef41Sopenharmony_ci } 781cb0ef41Sopenharmony_ci 791cb0ef41Sopenharmony_ci emit (ev, data) { 801cb0ef41Sopenharmony_ci if (ev === 'end') { 811cb0ef41Sopenharmony_ci this.#onEnd() 821cb0ef41Sopenharmony_ci } 831cb0ef41Sopenharmony_ci return super.emit(ev, data) 841cb0ef41Sopenharmony_ci } 851cb0ef41Sopenharmony_ci 861cb0ef41Sopenharmony_ci write (data) { 871cb0ef41Sopenharmony_ci this.size += data.length 881cb0ef41Sopenharmony_ci this.hashes.forEach(h => h.update(data)) 891cb0ef41Sopenharmony_ci return super.write(data) 901cb0ef41Sopenharmony_ci } 911cb0ef41Sopenharmony_ci 921cb0ef41Sopenharmony_ci #onEnd () { 931cb0ef41Sopenharmony_ci if (!this.goodSri) { 941cb0ef41Sopenharmony_ci this.#getOptions() 951cb0ef41Sopenharmony_ci } 961cb0ef41Sopenharmony_ci const newSri = parse(this.hashes.map((h, i) => { 971cb0ef41Sopenharmony_ci return `${this.algorithms[i]}-${h.digest('base64')}${this.optString}` 981cb0ef41Sopenharmony_ci }).join(' '), this.opts) 991cb0ef41Sopenharmony_ci // Integrity verification mode 1001cb0ef41Sopenharmony_ci const match = this.goodSri && newSri.match(this.sri, this.opts) 1011cb0ef41Sopenharmony_ci if (typeof this.expectedSize === 'number' && this.size !== this.expectedSize) { 1021cb0ef41Sopenharmony_ci /* eslint-disable-next-line max-len */ 1031cb0ef41Sopenharmony_ci const err = new Error(`stream size mismatch when checking ${this.sri}.\n Wanted: ${this.expectedSize}\n Found: ${this.size}`) 1041cb0ef41Sopenharmony_ci err.code = 'EBADSIZE' 1051cb0ef41Sopenharmony_ci err.found = this.size 1061cb0ef41Sopenharmony_ci err.expected = this.expectedSize 1071cb0ef41Sopenharmony_ci err.sri = this.sri 1081cb0ef41Sopenharmony_ci this.emit('error', err) 1091cb0ef41Sopenharmony_ci } else if (this.sri && !match) { 1101cb0ef41Sopenharmony_ci /* eslint-disable-next-line max-len */ 1111cb0ef41Sopenharmony_ci const err = new Error(`${this.sri} integrity checksum failed when using ${this.algorithm}: wanted ${this.digests} but got ${newSri}. (${this.size} bytes)`) 1121cb0ef41Sopenharmony_ci err.code = 'EINTEGRITY' 1131cb0ef41Sopenharmony_ci err.found = newSri 1141cb0ef41Sopenharmony_ci err.expected = this.digests 1151cb0ef41Sopenharmony_ci err.algorithm = this.algorithm 1161cb0ef41Sopenharmony_ci err.sri = this.sri 1171cb0ef41Sopenharmony_ci this.emit('error', err) 1181cb0ef41Sopenharmony_ci } else { 1191cb0ef41Sopenharmony_ci this.#emittedSize = this.size 1201cb0ef41Sopenharmony_ci this.emit('size', this.size) 1211cb0ef41Sopenharmony_ci this.#emittedIntegrity = newSri 1221cb0ef41Sopenharmony_ci this.emit('integrity', newSri) 1231cb0ef41Sopenharmony_ci if (match) { 1241cb0ef41Sopenharmony_ci this.#emittedVerified = match 1251cb0ef41Sopenharmony_ci this.emit('verified', match) 1261cb0ef41Sopenharmony_ci } 1271cb0ef41Sopenharmony_ci } 1281cb0ef41Sopenharmony_ci } 1291cb0ef41Sopenharmony_ci} 1301cb0ef41Sopenharmony_ci 1311cb0ef41Sopenharmony_ciclass Hash { 1321cb0ef41Sopenharmony_ci get isHash () { 1331cb0ef41Sopenharmony_ci return true 1341cb0ef41Sopenharmony_ci } 1351cb0ef41Sopenharmony_ci 1361cb0ef41Sopenharmony_ci constructor (hash, opts) { 1371cb0ef41Sopenharmony_ci const strict = opts?.strict 1381cb0ef41Sopenharmony_ci this.source = hash.trim() 1391cb0ef41Sopenharmony_ci 1401cb0ef41Sopenharmony_ci // set default values so that we make V8 happy to 1411cb0ef41Sopenharmony_ci // always see a familiar object template. 1421cb0ef41Sopenharmony_ci this.digest = '' 1431cb0ef41Sopenharmony_ci this.algorithm = '' 1441cb0ef41Sopenharmony_ci this.options = [] 1451cb0ef41Sopenharmony_ci 1461cb0ef41Sopenharmony_ci // 3.1. Integrity metadata (called "Hash" by ssri) 1471cb0ef41Sopenharmony_ci // https://w3c.github.io/webappsec-subresource-integrity/#integrity-metadata-description 1481cb0ef41Sopenharmony_ci const match = this.source.match( 1491cb0ef41Sopenharmony_ci strict 1501cb0ef41Sopenharmony_ci ? STRICT_SRI_REGEX 1511cb0ef41Sopenharmony_ci : SRI_REGEX 1521cb0ef41Sopenharmony_ci ) 1531cb0ef41Sopenharmony_ci if (!match) { 1541cb0ef41Sopenharmony_ci return 1551cb0ef41Sopenharmony_ci } 1561cb0ef41Sopenharmony_ci if (strict && !SPEC_ALGORITHMS.includes(match[1])) { 1571cb0ef41Sopenharmony_ci return 1581cb0ef41Sopenharmony_ci } 1591cb0ef41Sopenharmony_ci this.algorithm = match[1] 1601cb0ef41Sopenharmony_ci this.digest = match[2] 1611cb0ef41Sopenharmony_ci 1621cb0ef41Sopenharmony_ci const rawOpts = match[3] 1631cb0ef41Sopenharmony_ci if (rawOpts) { 1641cb0ef41Sopenharmony_ci this.options = rawOpts.slice(1).split('?') 1651cb0ef41Sopenharmony_ci } 1661cb0ef41Sopenharmony_ci } 1671cb0ef41Sopenharmony_ci 1681cb0ef41Sopenharmony_ci hexDigest () { 1691cb0ef41Sopenharmony_ci return this.digest && Buffer.from(this.digest, 'base64').toString('hex') 1701cb0ef41Sopenharmony_ci } 1711cb0ef41Sopenharmony_ci 1721cb0ef41Sopenharmony_ci toJSON () { 1731cb0ef41Sopenharmony_ci return this.toString() 1741cb0ef41Sopenharmony_ci } 1751cb0ef41Sopenharmony_ci 1761cb0ef41Sopenharmony_ci match (integrity, opts) { 1771cb0ef41Sopenharmony_ci const other = parse(integrity, opts) 1781cb0ef41Sopenharmony_ci if (!other) { 1791cb0ef41Sopenharmony_ci return false 1801cb0ef41Sopenharmony_ci } 1811cb0ef41Sopenharmony_ci if (other.isIntegrity) { 1821cb0ef41Sopenharmony_ci const algo = other.pickAlgorithm(opts, [this.algorithm]) 1831cb0ef41Sopenharmony_ci 1841cb0ef41Sopenharmony_ci if (!algo) { 1851cb0ef41Sopenharmony_ci return false 1861cb0ef41Sopenharmony_ci } 1871cb0ef41Sopenharmony_ci 1881cb0ef41Sopenharmony_ci const foundHash = other[algo].find(hash => hash.digest === this.digest) 1891cb0ef41Sopenharmony_ci 1901cb0ef41Sopenharmony_ci if (foundHash) { 1911cb0ef41Sopenharmony_ci return foundHash 1921cb0ef41Sopenharmony_ci } 1931cb0ef41Sopenharmony_ci 1941cb0ef41Sopenharmony_ci return false 1951cb0ef41Sopenharmony_ci } 1961cb0ef41Sopenharmony_ci return other.digest === this.digest ? other : false 1971cb0ef41Sopenharmony_ci } 1981cb0ef41Sopenharmony_ci 1991cb0ef41Sopenharmony_ci toString (opts) { 2001cb0ef41Sopenharmony_ci if (opts?.strict) { 2011cb0ef41Sopenharmony_ci // Strict mode enforces the standard as close to the foot of the 2021cb0ef41Sopenharmony_ci // letter as it can. 2031cb0ef41Sopenharmony_ci if (!( 2041cb0ef41Sopenharmony_ci // The spec has very restricted productions for algorithms. 2051cb0ef41Sopenharmony_ci // https://www.w3.org/TR/CSP2/#source-list-syntax 2061cb0ef41Sopenharmony_ci SPEC_ALGORITHMS.includes(this.algorithm) && 2071cb0ef41Sopenharmony_ci // Usually, if someone insists on using a "different" base64, we 2081cb0ef41Sopenharmony_ci // leave it as-is, since there's multiple standards, and the 2091cb0ef41Sopenharmony_ci // specified is not a URL-safe variant. 2101cb0ef41Sopenharmony_ci // https://www.w3.org/TR/CSP2/#base64_value 2111cb0ef41Sopenharmony_ci this.digest.match(BASE64_REGEX) && 2121cb0ef41Sopenharmony_ci // Option syntax is strictly visual chars. 2131cb0ef41Sopenharmony_ci // https://w3c.github.io/webappsec-subresource-integrity/#grammardef-option-expression 2141cb0ef41Sopenharmony_ci // https://tools.ietf.org/html/rfc5234#appendix-B.1 2151cb0ef41Sopenharmony_ci this.options.every(opt => opt.match(VCHAR_REGEX)) 2161cb0ef41Sopenharmony_ci )) { 2171cb0ef41Sopenharmony_ci return '' 2181cb0ef41Sopenharmony_ci } 2191cb0ef41Sopenharmony_ci } 2201cb0ef41Sopenharmony_ci return `${this.algorithm}-${this.digest}${getOptString(this.options)}` 2211cb0ef41Sopenharmony_ci } 2221cb0ef41Sopenharmony_ci} 2231cb0ef41Sopenharmony_ci 2241cb0ef41Sopenharmony_cifunction integrityHashToString (toString, sep, opts, hashes) { 2251cb0ef41Sopenharmony_ci const toStringIsNotEmpty = toString !== '' 2261cb0ef41Sopenharmony_ci 2271cb0ef41Sopenharmony_ci let shouldAddFirstSep = false 2281cb0ef41Sopenharmony_ci let complement = '' 2291cb0ef41Sopenharmony_ci 2301cb0ef41Sopenharmony_ci const lastIndex = hashes.length - 1 2311cb0ef41Sopenharmony_ci 2321cb0ef41Sopenharmony_ci for (let i = 0; i < lastIndex; i++) { 2331cb0ef41Sopenharmony_ci const hashString = Hash.prototype.toString.call(hashes[i], opts) 2341cb0ef41Sopenharmony_ci 2351cb0ef41Sopenharmony_ci if (hashString) { 2361cb0ef41Sopenharmony_ci shouldAddFirstSep = true 2371cb0ef41Sopenharmony_ci 2381cb0ef41Sopenharmony_ci complement += hashString 2391cb0ef41Sopenharmony_ci complement += sep 2401cb0ef41Sopenharmony_ci } 2411cb0ef41Sopenharmony_ci } 2421cb0ef41Sopenharmony_ci 2431cb0ef41Sopenharmony_ci const finalHashString = Hash.prototype.toString.call(hashes[lastIndex], opts) 2441cb0ef41Sopenharmony_ci 2451cb0ef41Sopenharmony_ci if (finalHashString) { 2461cb0ef41Sopenharmony_ci shouldAddFirstSep = true 2471cb0ef41Sopenharmony_ci complement += finalHashString 2481cb0ef41Sopenharmony_ci } 2491cb0ef41Sopenharmony_ci 2501cb0ef41Sopenharmony_ci if (toStringIsNotEmpty && shouldAddFirstSep) { 2511cb0ef41Sopenharmony_ci return toString + sep + complement 2521cb0ef41Sopenharmony_ci } 2531cb0ef41Sopenharmony_ci 2541cb0ef41Sopenharmony_ci return toString + complement 2551cb0ef41Sopenharmony_ci} 2561cb0ef41Sopenharmony_ci 2571cb0ef41Sopenharmony_ciclass Integrity { 2581cb0ef41Sopenharmony_ci get isIntegrity () { 2591cb0ef41Sopenharmony_ci return true 2601cb0ef41Sopenharmony_ci } 2611cb0ef41Sopenharmony_ci 2621cb0ef41Sopenharmony_ci toJSON () { 2631cb0ef41Sopenharmony_ci return this.toString() 2641cb0ef41Sopenharmony_ci } 2651cb0ef41Sopenharmony_ci 2661cb0ef41Sopenharmony_ci isEmpty () { 2671cb0ef41Sopenharmony_ci return Object.keys(this).length === 0 2681cb0ef41Sopenharmony_ci } 2691cb0ef41Sopenharmony_ci 2701cb0ef41Sopenharmony_ci toString (opts) { 2711cb0ef41Sopenharmony_ci let sep = opts?.sep || ' ' 2721cb0ef41Sopenharmony_ci let toString = '' 2731cb0ef41Sopenharmony_ci 2741cb0ef41Sopenharmony_ci if (opts?.strict) { 2751cb0ef41Sopenharmony_ci // Entries must be separated by whitespace, according to spec. 2761cb0ef41Sopenharmony_ci sep = sep.replace(/\S+/g, ' ') 2771cb0ef41Sopenharmony_ci 2781cb0ef41Sopenharmony_ci for (const hash of SPEC_ALGORITHMS) { 2791cb0ef41Sopenharmony_ci if (this[hash]) { 2801cb0ef41Sopenharmony_ci toString = integrityHashToString(toString, sep, opts, this[hash]) 2811cb0ef41Sopenharmony_ci } 2821cb0ef41Sopenharmony_ci } 2831cb0ef41Sopenharmony_ci } else { 2841cb0ef41Sopenharmony_ci for (const hash of Object.keys(this)) { 2851cb0ef41Sopenharmony_ci toString = integrityHashToString(toString, sep, opts, this[hash]) 2861cb0ef41Sopenharmony_ci } 2871cb0ef41Sopenharmony_ci } 2881cb0ef41Sopenharmony_ci 2891cb0ef41Sopenharmony_ci return toString 2901cb0ef41Sopenharmony_ci } 2911cb0ef41Sopenharmony_ci 2921cb0ef41Sopenharmony_ci concat (integrity, opts) { 2931cb0ef41Sopenharmony_ci const other = typeof integrity === 'string' 2941cb0ef41Sopenharmony_ci ? integrity 2951cb0ef41Sopenharmony_ci : stringify(integrity, opts) 2961cb0ef41Sopenharmony_ci return parse(`${this.toString(opts)} ${other}`, opts) 2971cb0ef41Sopenharmony_ci } 2981cb0ef41Sopenharmony_ci 2991cb0ef41Sopenharmony_ci hexDigest () { 3001cb0ef41Sopenharmony_ci return parse(this, { single: true }).hexDigest() 3011cb0ef41Sopenharmony_ci } 3021cb0ef41Sopenharmony_ci 3031cb0ef41Sopenharmony_ci // add additional hashes to an integrity value, but prevent 3041cb0ef41Sopenharmony_ci // *changing* an existing integrity hash. 3051cb0ef41Sopenharmony_ci merge (integrity, opts) { 3061cb0ef41Sopenharmony_ci const other = parse(integrity, opts) 3071cb0ef41Sopenharmony_ci for (const algo in other) { 3081cb0ef41Sopenharmony_ci if (this[algo]) { 3091cb0ef41Sopenharmony_ci if (!this[algo].find(hash => 3101cb0ef41Sopenharmony_ci other[algo].find(otherhash => 3111cb0ef41Sopenharmony_ci hash.digest === otherhash.digest))) { 3121cb0ef41Sopenharmony_ci throw new Error('hashes do not match, cannot update integrity') 3131cb0ef41Sopenharmony_ci } 3141cb0ef41Sopenharmony_ci } else { 3151cb0ef41Sopenharmony_ci this[algo] = other[algo] 3161cb0ef41Sopenharmony_ci } 3171cb0ef41Sopenharmony_ci } 3181cb0ef41Sopenharmony_ci } 3191cb0ef41Sopenharmony_ci 3201cb0ef41Sopenharmony_ci match (integrity, opts) { 3211cb0ef41Sopenharmony_ci const other = parse(integrity, opts) 3221cb0ef41Sopenharmony_ci if (!other) { 3231cb0ef41Sopenharmony_ci return false 3241cb0ef41Sopenharmony_ci } 3251cb0ef41Sopenharmony_ci const algo = other.pickAlgorithm(opts, Object.keys(this)) 3261cb0ef41Sopenharmony_ci return ( 3271cb0ef41Sopenharmony_ci !!algo && 3281cb0ef41Sopenharmony_ci this[algo] && 3291cb0ef41Sopenharmony_ci other[algo] && 3301cb0ef41Sopenharmony_ci this[algo].find(hash => 3311cb0ef41Sopenharmony_ci other[algo].find(otherhash => 3321cb0ef41Sopenharmony_ci hash.digest === otherhash.digest 3331cb0ef41Sopenharmony_ci ) 3341cb0ef41Sopenharmony_ci ) 3351cb0ef41Sopenharmony_ci ) || false 3361cb0ef41Sopenharmony_ci } 3371cb0ef41Sopenharmony_ci 3381cb0ef41Sopenharmony_ci // Pick the highest priority algorithm present, optionally also limited to a 3391cb0ef41Sopenharmony_ci // set of hashes found in another integrity. When limiting it may return 3401cb0ef41Sopenharmony_ci // nothing. 3411cb0ef41Sopenharmony_ci pickAlgorithm (opts, hashes) { 3421cb0ef41Sopenharmony_ci const pickAlgorithm = opts?.pickAlgorithm || getPrioritizedHash 3431cb0ef41Sopenharmony_ci const keys = Object.keys(this).filter(k => { 3441cb0ef41Sopenharmony_ci if (hashes?.length) { 3451cb0ef41Sopenharmony_ci return hashes.includes(k) 3461cb0ef41Sopenharmony_ci } 3471cb0ef41Sopenharmony_ci return true 3481cb0ef41Sopenharmony_ci }) 3491cb0ef41Sopenharmony_ci if (keys.length) { 3501cb0ef41Sopenharmony_ci return keys.reduce((acc, algo) => pickAlgorithm(acc, algo) || acc) 3511cb0ef41Sopenharmony_ci } 3521cb0ef41Sopenharmony_ci // no intersection between this and hashes, 3531cb0ef41Sopenharmony_ci return null 3541cb0ef41Sopenharmony_ci } 3551cb0ef41Sopenharmony_ci} 3561cb0ef41Sopenharmony_ci 3571cb0ef41Sopenharmony_cimodule.exports.parse = parse 3581cb0ef41Sopenharmony_cifunction parse (sri, opts) { 3591cb0ef41Sopenharmony_ci if (!sri) { 3601cb0ef41Sopenharmony_ci return null 3611cb0ef41Sopenharmony_ci } 3621cb0ef41Sopenharmony_ci if (typeof sri === 'string') { 3631cb0ef41Sopenharmony_ci return _parse(sri, opts) 3641cb0ef41Sopenharmony_ci } else if (sri.algorithm && sri.digest) { 3651cb0ef41Sopenharmony_ci const fullSri = new Integrity() 3661cb0ef41Sopenharmony_ci fullSri[sri.algorithm] = [sri] 3671cb0ef41Sopenharmony_ci return _parse(stringify(fullSri, opts), opts) 3681cb0ef41Sopenharmony_ci } else { 3691cb0ef41Sopenharmony_ci return _parse(stringify(sri, opts), opts) 3701cb0ef41Sopenharmony_ci } 3711cb0ef41Sopenharmony_ci} 3721cb0ef41Sopenharmony_ci 3731cb0ef41Sopenharmony_cifunction _parse (integrity, opts) { 3741cb0ef41Sopenharmony_ci // 3.4.3. Parse metadata 3751cb0ef41Sopenharmony_ci // https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata 3761cb0ef41Sopenharmony_ci if (opts?.single) { 3771cb0ef41Sopenharmony_ci return new Hash(integrity, opts) 3781cb0ef41Sopenharmony_ci } 3791cb0ef41Sopenharmony_ci const hashes = integrity.trim().split(/\s+/).reduce((acc, string) => { 3801cb0ef41Sopenharmony_ci const hash = new Hash(string, opts) 3811cb0ef41Sopenharmony_ci if (hash.algorithm && hash.digest) { 3821cb0ef41Sopenharmony_ci const algo = hash.algorithm 3831cb0ef41Sopenharmony_ci if (!acc[algo]) { 3841cb0ef41Sopenharmony_ci acc[algo] = [] 3851cb0ef41Sopenharmony_ci } 3861cb0ef41Sopenharmony_ci acc[algo].push(hash) 3871cb0ef41Sopenharmony_ci } 3881cb0ef41Sopenharmony_ci return acc 3891cb0ef41Sopenharmony_ci }, new Integrity()) 3901cb0ef41Sopenharmony_ci return hashes.isEmpty() ? null : hashes 3911cb0ef41Sopenharmony_ci} 3921cb0ef41Sopenharmony_ci 3931cb0ef41Sopenharmony_cimodule.exports.stringify = stringify 3941cb0ef41Sopenharmony_cifunction stringify (obj, opts) { 3951cb0ef41Sopenharmony_ci if (obj.algorithm && obj.digest) { 3961cb0ef41Sopenharmony_ci return Hash.prototype.toString.call(obj, opts) 3971cb0ef41Sopenharmony_ci } else if (typeof obj === 'string') { 3981cb0ef41Sopenharmony_ci return stringify(parse(obj, opts), opts) 3991cb0ef41Sopenharmony_ci } else { 4001cb0ef41Sopenharmony_ci return Integrity.prototype.toString.call(obj, opts) 4011cb0ef41Sopenharmony_ci } 4021cb0ef41Sopenharmony_ci} 4031cb0ef41Sopenharmony_ci 4041cb0ef41Sopenharmony_cimodule.exports.fromHex = fromHex 4051cb0ef41Sopenharmony_cifunction fromHex (hexDigest, algorithm, opts) { 4061cb0ef41Sopenharmony_ci const optString = getOptString(opts?.options) 4071cb0ef41Sopenharmony_ci return parse( 4081cb0ef41Sopenharmony_ci `${algorithm}-${ 4091cb0ef41Sopenharmony_ci Buffer.from(hexDigest, 'hex').toString('base64') 4101cb0ef41Sopenharmony_ci }${optString}`, opts 4111cb0ef41Sopenharmony_ci ) 4121cb0ef41Sopenharmony_ci} 4131cb0ef41Sopenharmony_ci 4141cb0ef41Sopenharmony_cimodule.exports.fromData = fromData 4151cb0ef41Sopenharmony_cifunction fromData (data, opts) { 4161cb0ef41Sopenharmony_ci const algorithms = opts?.algorithms || [...DEFAULT_ALGORITHMS] 4171cb0ef41Sopenharmony_ci const optString = getOptString(opts?.options) 4181cb0ef41Sopenharmony_ci return algorithms.reduce((acc, algo) => { 4191cb0ef41Sopenharmony_ci const digest = crypto.createHash(algo).update(data).digest('base64') 4201cb0ef41Sopenharmony_ci const hash = new Hash( 4211cb0ef41Sopenharmony_ci `${algo}-${digest}${optString}`, 4221cb0ef41Sopenharmony_ci opts 4231cb0ef41Sopenharmony_ci ) 4241cb0ef41Sopenharmony_ci /* istanbul ignore else - it would be VERY strange if the string we 4251cb0ef41Sopenharmony_ci * just calculated with an algo did not have an algo or digest. 4261cb0ef41Sopenharmony_ci */ 4271cb0ef41Sopenharmony_ci if (hash.algorithm && hash.digest) { 4281cb0ef41Sopenharmony_ci const hashAlgo = hash.algorithm 4291cb0ef41Sopenharmony_ci if (!acc[hashAlgo]) { 4301cb0ef41Sopenharmony_ci acc[hashAlgo] = [] 4311cb0ef41Sopenharmony_ci } 4321cb0ef41Sopenharmony_ci acc[hashAlgo].push(hash) 4331cb0ef41Sopenharmony_ci } 4341cb0ef41Sopenharmony_ci return acc 4351cb0ef41Sopenharmony_ci }, new Integrity()) 4361cb0ef41Sopenharmony_ci} 4371cb0ef41Sopenharmony_ci 4381cb0ef41Sopenharmony_cimodule.exports.fromStream = fromStream 4391cb0ef41Sopenharmony_cifunction fromStream (stream, opts) { 4401cb0ef41Sopenharmony_ci const istream = integrityStream(opts) 4411cb0ef41Sopenharmony_ci return new Promise((resolve, reject) => { 4421cb0ef41Sopenharmony_ci stream.pipe(istream) 4431cb0ef41Sopenharmony_ci stream.on('error', reject) 4441cb0ef41Sopenharmony_ci istream.on('error', reject) 4451cb0ef41Sopenharmony_ci let sri 4461cb0ef41Sopenharmony_ci istream.on('integrity', s => { 4471cb0ef41Sopenharmony_ci sri = s 4481cb0ef41Sopenharmony_ci }) 4491cb0ef41Sopenharmony_ci istream.on('end', () => resolve(sri)) 4501cb0ef41Sopenharmony_ci istream.resume() 4511cb0ef41Sopenharmony_ci }) 4521cb0ef41Sopenharmony_ci} 4531cb0ef41Sopenharmony_ci 4541cb0ef41Sopenharmony_cimodule.exports.checkData = checkData 4551cb0ef41Sopenharmony_cifunction checkData (data, sri, opts) { 4561cb0ef41Sopenharmony_ci sri = parse(sri, opts) 4571cb0ef41Sopenharmony_ci if (!sri || !Object.keys(sri).length) { 4581cb0ef41Sopenharmony_ci if (opts?.error) { 4591cb0ef41Sopenharmony_ci throw Object.assign( 4601cb0ef41Sopenharmony_ci new Error('No valid integrity hashes to check against'), { 4611cb0ef41Sopenharmony_ci code: 'EINTEGRITY', 4621cb0ef41Sopenharmony_ci } 4631cb0ef41Sopenharmony_ci ) 4641cb0ef41Sopenharmony_ci } else { 4651cb0ef41Sopenharmony_ci return false 4661cb0ef41Sopenharmony_ci } 4671cb0ef41Sopenharmony_ci } 4681cb0ef41Sopenharmony_ci const algorithm = sri.pickAlgorithm(opts) 4691cb0ef41Sopenharmony_ci const digest = crypto.createHash(algorithm).update(data).digest('base64') 4701cb0ef41Sopenharmony_ci const newSri = parse({ algorithm, digest }) 4711cb0ef41Sopenharmony_ci const match = newSri.match(sri, opts) 4721cb0ef41Sopenharmony_ci opts = opts || {} 4731cb0ef41Sopenharmony_ci if (match || !(opts.error)) { 4741cb0ef41Sopenharmony_ci return match 4751cb0ef41Sopenharmony_ci } else if (typeof opts.size === 'number' && (data.length !== opts.size)) { 4761cb0ef41Sopenharmony_ci /* eslint-disable-next-line max-len */ 4771cb0ef41Sopenharmony_ci const err = new Error(`data size mismatch when checking ${sri}.\n Wanted: ${opts.size}\n Found: ${data.length}`) 4781cb0ef41Sopenharmony_ci err.code = 'EBADSIZE' 4791cb0ef41Sopenharmony_ci err.found = data.length 4801cb0ef41Sopenharmony_ci err.expected = opts.size 4811cb0ef41Sopenharmony_ci err.sri = sri 4821cb0ef41Sopenharmony_ci throw err 4831cb0ef41Sopenharmony_ci } else { 4841cb0ef41Sopenharmony_ci /* eslint-disable-next-line max-len */ 4851cb0ef41Sopenharmony_ci const err = new Error(`Integrity checksum failed when using ${algorithm}: Wanted ${sri}, but got ${newSri}. (${data.length} bytes)`) 4861cb0ef41Sopenharmony_ci err.code = 'EINTEGRITY' 4871cb0ef41Sopenharmony_ci err.found = newSri 4881cb0ef41Sopenharmony_ci err.expected = sri 4891cb0ef41Sopenharmony_ci err.algorithm = algorithm 4901cb0ef41Sopenharmony_ci err.sri = sri 4911cb0ef41Sopenharmony_ci throw err 4921cb0ef41Sopenharmony_ci } 4931cb0ef41Sopenharmony_ci} 4941cb0ef41Sopenharmony_ci 4951cb0ef41Sopenharmony_cimodule.exports.checkStream = checkStream 4961cb0ef41Sopenharmony_cifunction checkStream (stream, sri, opts) { 4971cb0ef41Sopenharmony_ci opts = opts || Object.create(null) 4981cb0ef41Sopenharmony_ci opts.integrity = sri 4991cb0ef41Sopenharmony_ci sri = parse(sri, opts) 5001cb0ef41Sopenharmony_ci if (!sri || !Object.keys(sri).length) { 5011cb0ef41Sopenharmony_ci return Promise.reject(Object.assign( 5021cb0ef41Sopenharmony_ci new Error('No valid integrity hashes to check against'), { 5031cb0ef41Sopenharmony_ci code: 'EINTEGRITY', 5041cb0ef41Sopenharmony_ci } 5051cb0ef41Sopenharmony_ci )) 5061cb0ef41Sopenharmony_ci } 5071cb0ef41Sopenharmony_ci const checker = integrityStream(opts) 5081cb0ef41Sopenharmony_ci return new Promise((resolve, reject) => { 5091cb0ef41Sopenharmony_ci stream.pipe(checker) 5101cb0ef41Sopenharmony_ci stream.on('error', reject) 5111cb0ef41Sopenharmony_ci checker.on('error', reject) 5121cb0ef41Sopenharmony_ci let verified 5131cb0ef41Sopenharmony_ci checker.on('verified', s => { 5141cb0ef41Sopenharmony_ci verified = s 5151cb0ef41Sopenharmony_ci }) 5161cb0ef41Sopenharmony_ci checker.on('end', () => resolve(verified)) 5171cb0ef41Sopenharmony_ci checker.resume() 5181cb0ef41Sopenharmony_ci }) 5191cb0ef41Sopenharmony_ci} 5201cb0ef41Sopenharmony_ci 5211cb0ef41Sopenharmony_cimodule.exports.integrityStream = integrityStream 5221cb0ef41Sopenharmony_cifunction integrityStream (opts = Object.create(null)) { 5231cb0ef41Sopenharmony_ci return new IntegrityStream(opts) 5241cb0ef41Sopenharmony_ci} 5251cb0ef41Sopenharmony_ci 5261cb0ef41Sopenharmony_cimodule.exports.create = createIntegrity 5271cb0ef41Sopenharmony_cifunction createIntegrity (opts) { 5281cb0ef41Sopenharmony_ci const algorithms = opts?.algorithms || [...DEFAULT_ALGORITHMS] 5291cb0ef41Sopenharmony_ci const optString = getOptString(opts?.options) 5301cb0ef41Sopenharmony_ci 5311cb0ef41Sopenharmony_ci const hashes = algorithms.map(crypto.createHash) 5321cb0ef41Sopenharmony_ci 5331cb0ef41Sopenharmony_ci return { 5341cb0ef41Sopenharmony_ci update: function (chunk, enc) { 5351cb0ef41Sopenharmony_ci hashes.forEach(h => h.update(chunk, enc)) 5361cb0ef41Sopenharmony_ci return this 5371cb0ef41Sopenharmony_ci }, 5381cb0ef41Sopenharmony_ci digest: function (enc) { 5391cb0ef41Sopenharmony_ci const integrity = algorithms.reduce((acc, algo) => { 5401cb0ef41Sopenharmony_ci const digest = hashes.shift().digest('base64') 5411cb0ef41Sopenharmony_ci const hash = new Hash( 5421cb0ef41Sopenharmony_ci `${algo}-${digest}${optString}`, 5431cb0ef41Sopenharmony_ci opts 5441cb0ef41Sopenharmony_ci ) 5451cb0ef41Sopenharmony_ci /* istanbul ignore else - it would be VERY strange if the hash we 5461cb0ef41Sopenharmony_ci * just calculated with an algo did not have an algo or digest. 5471cb0ef41Sopenharmony_ci */ 5481cb0ef41Sopenharmony_ci if (hash.algorithm && hash.digest) { 5491cb0ef41Sopenharmony_ci const hashAlgo = hash.algorithm 5501cb0ef41Sopenharmony_ci if (!acc[hashAlgo]) { 5511cb0ef41Sopenharmony_ci acc[hashAlgo] = [] 5521cb0ef41Sopenharmony_ci } 5531cb0ef41Sopenharmony_ci acc[hashAlgo].push(hash) 5541cb0ef41Sopenharmony_ci } 5551cb0ef41Sopenharmony_ci return acc 5561cb0ef41Sopenharmony_ci }, new Integrity()) 5571cb0ef41Sopenharmony_ci 5581cb0ef41Sopenharmony_ci return integrity 5591cb0ef41Sopenharmony_ci }, 5601cb0ef41Sopenharmony_ci } 5611cb0ef41Sopenharmony_ci} 5621cb0ef41Sopenharmony_ci 5631cb0ef41Sopenharmony_ciconst NODE_HASHES = crypto.getHashes() 5641cb0ef41Sopenharmony_ci 5651cb0ef41Sopenharmony_ci// This is a Best Effort™ at a reasonable priority for hash algos 5661cb0ef41Sopenharmony_ciconst DEFAULT_PRIORITY = [ 5671cb0ef41Sopenharmony_ci 'md5', 'whirlpool', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512', 5681cb0ef41Sopenharmony_ci // TODO - it's unclear _which_ of these Node will actually use as its name 5691cb0ef41Sopenharmony_ci // for the algorithm, so we guesswork it based on the OpenSSL names. 5701cb0ef41Sopenharmony_ci 'sha3', 5711cb0ef41Sopenharmony_ci 'sha3-256', 'sha3-384', 'sha3-512', 5721cb0ef41Sopenharmony_ci 'sha3_256', 'sha3_384', 'sha3_512', 5731cb0ef41Sopenharmony_ci].filter(algo => NODE_HASHES.includes(algo)) 5741cb0ef41Sopenharmony_ci 5751cb0ef41Sopenharmony_cifunction getPrioritizedHash (algo1, algo2) { 5761cb0ef41Sopenharmony_ci /* eslint-disable-next-line max-len */ 5771cb0ef41Sopenharmony_ci return DEFAULT_PRIORITY.indexOf(algo1.toLowerCase()) >= DEFAULT_PRIORITY.indexOf(algo2.toLowerCase()) 5781cb0ef41Sopenharmony_ci ? algo1 5791cb0ef41Sopenharmony_ci : algo2 5801cb0ef41Sopenharmony_ci} 581