diff --git a/lib/index.js b/lib/index.js index 222861a..d930add 100644 --- a/lib/index.js +++ b/lib/index.js @@ -4,6 +4,7 @@ const crypto = require('crypto') const MiniPass = require('minipass') const SPEC_ALGORITHMS = ['sha256', 'sha384', 'sha512'] +const DEFAULT_ALGORITHMS = ['sha512'] // TODO: this should really be a hardcoded list of algorithms we support, // rather than [a-z0-9]. @@ -12,21 +13,7 @@ const SRI_REGEX = /^([a-z0-9]+)-([^?]+)([?\S*]*)$/ const STRICT_SRI_REGEX = /^([a-z0-9]+)-([A-Za-z0-9+/=]{44,88})(\?[\x21-\x7E]*)?$/ const VCHAR_REGEX = /^[\x21-\x7E]+$/ -const defaultOpts = { - algorithms: ['sha512'], - error: false, - options: [], - pickAlgorithm: getPrioritizedHash, - sep: ' ', - single: false, - strict: false, -} - -const ssriOpts = (opts = {}) => ({ ...defaultOpts, ...opts }) - -const getOptString = options => !options || !options.length - ? '' - : `?${options.join('?')}` +const getOptString = options => options?.length ? `?${options.join('?')}` : '' const _onEnd = Symbol('_onEnd') const _getOptions = Symbol('_getOptions') @@ -44,7 +31,7 @@ class IntegrityStream extends MiniPass { this[_getOptions]() // options used for calculating stream. can't be changed. - const { algorithms = defaultOpts.algorithms } = opts + const algorithms = opts?.algorithms || DEFAULT_ALGORITHMS this.algorithms = Array.from( new Set(algorithms.concat(this.algorithm ? [this.algorithm] : [])) ) @@ -52,19 +39,13 @@ class IntegrityStream extends MiniPass { } [_getOptions] () { - const { - integrity, - size, - options, - } = { ...defaultOpts, ...this.opts } - // For verification - this.sri = integrity ? parse(integrity, this.opts) : null - this.expectedSize = size + this.sri = this.opts?.integrity ? parse(this.opts?.integrity, this.opts) : null + this.expectedSize = this.opts?.size this.goodSri = this.sri ? !!Object.keys(this.sri).length : false this.algorithm = this.goodSri ? this.sri.pickAlgorithm(this.opts) : null this.digests = this.goodSri ? this.sri[this.algorithm] : null - this.optString = getOptString(options) + this.optString = getOptString(this.opts?.options) } on (ev, handler) { @@ -141,8 +122,7 @@ class Hash { } constructor (hash, opts) { - opts = ssriOpts(opts) - const strict = !!opts.strict + const strict = opts?.strict this.source = hash.trim() // set default values so that we make V8 happy to @@ -161,7 +141,7 @@ class Hash { if (!match) { return } - if (strict && !SPEC_ALGORITHMS.some(a => a === match[1])) { + if (strict && !SPEC_ALGORITHMS.includes(match[1])) { return } this.algorithm = match[1] @@ -182,14 +162,13 @@ class Hash { } toString (opts) { - opts = ssriOpts(opts) - if (opts.strict) { + if (opts?.strict) { // Strict mode enforces the standard as close to the foot of the // letter as it can. if (!( // The spec has very restricted productions for algorithms. // https://www.w3.org/TR/CSP2/#source-list-syntax - SPEC_ALGORITHMS.some(x => x === this.algorithm) && + SPEC_ALGORITHMS.includes(this.algorithm) && // Usually, if someone insists on using a "different" base64, we // leave it as-is, since there's multiple standards, and the // specified is not a URL-safe variant. @@ -203,10 +182,7 @@ class Hash { return '' } } - const options = this.options && this.options.length - ? `?${this.options.join('?')}` - : '' - return `${this.algorithm}-${this.digest}${options}` + return `${this.algorithm}-${this.digest}${getOptString(this.options)}` } } @@ -224,9 +200,8 @@ class Integrity { } toString (opts) { - opts = ssriOpts(opts) - let sep = opts.sep || ' ' - if (opts.strict) { + let sep = opts?.sep || ' ' + if (opts?.strict) { // Entries must be separated by whitespace, according to spec. sep = sep.replace(/\S+/g, ' ') } @@ -238,7 +213,6 @@ class Integrity { } concat (integrity, opts) { - opts = ssriOpts(opts) const other = typeof integrity === 'string' ? integrity : stringify(integrity, opts) @@ -252,7 +226,6 @@ class Integrity { // add additional hashes to an integrity value, but prevent // *changing* an existing integrity hash. merge (integrity, opts) { - opts = ssriOpts(opts) const other = parse(integrity, opts) for (const algo in other) { if (this[algo]) { @@ -268,7 +241,6 @@ class Integrity { } match (integrity, opts) { - opts = ssriOpts(opts) const other = parse(integrity, opts) if (!other) { return false @@ -286,8 +258,7 @@ class Integrity { } pickAlgorithm (opts) { - opts = ssriOpts(opts) - const pickAlgorithm = opts.pickAlgorithm + const pickAlgorithm = opts?.pickAlgorithm || getPrioritizedHash const keys = Object.keys(this) return keys.reduce((acc, algo) => { return pickAlgorithm(acc, algo) || acc @@ -300,7 +271,6 @@ function parse (sri, opts) { if (!sri) { return null } - opts = ssriOpts(opts) if (typeof sri === 'string') { return _parse(sri, opts) } else if (sri.algorithm && sri.digest) { @@ -315,7 +285,7 @@ function parse (sri, opts) { function _parse (integrity, opts) { // 3.4.3. Parse metadata // https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata - if (opts.single) { + if (opts?.single) { return new Hash(integrity, opts) } const hashes = integrity.trim().split(/\s+/).reduce((acc, string) => { @@ -334,7 +304,6 @@ function _parse (integrity, opts) { module.exports.stringify = stringify function stringify (obj, opts) { - opts = ssriOpts(opts) if (obj.algorithm && obj.digest) { return Hash.prototype.toString.call(obj, opts) } else if (typeof obj === 'string') { @@ -346,8 +315,7 @@ function stringify (obj, opts) { module.exports.fromHex = fromHex function fromHex (hexDigest, algorithm, opts) { - opts = ssriOpts(opts) - const optString = getOptString(opts.options) + const optString = getOptString(opts?.options) return parse( `${algorithm}-${ Buffer.from(hexDigest, 'hex').toString('base64') @@ -357,9 +325,8 @@ function fromHex (hexDigest, algorithm, opts) { module.exports.fromData = fromData function fromData (data, opts) { - opts = ssriOpts(opts) - const algorithms = opts.algorithms - const optString = getOptString(opts.options) + const algorithms = opts?.algorithms || DEFAULT_ALGORITHMS + const optString = getOptString(opts?.options) return algorithms.reduce((acc, algo) => { const digest = crypto.createHash(algo).update(data).digest('base64') const hash = new Hash( @@ -382,7 +349,6 @@ function fromData (data, opts) { module.exports.fromStream = fromStream function fromStream (stream, opts) { - opts = ssriOpts(opts) const istream = integrityStream(opts) return new Promise((resolve, reject) => { stream.pipe(istream) @@ -399,10 +365,9 @@ function fromStream (stream, opts) { module.exports.checkData = checkData function checkData (data, sri, opts) { - opts = ssriOpts(opts) sri = parse(sri, opts) if (!sri || !Object.keys(sri).length) { - if (opts.error) { + if (opts?.error) { throw Object.assign( new Error('No valid integrity hashes to check against'), { code: 'EINTEGRITY', @@ -416,7 +381,8 @@ function checkData (data, sri, opts) { const digest = crypto.createHash(algorithm).update(data).digest('base64') const newSri = parse({ algorithm, digest }) const match = newSri.match(sri, opts) - if (match || !opts.error) { + opts = opts || Object.create(null) + if (match || !(opts.error)) { return match } else if (typeof opts.size === 'number' && (data.length !== opts.size)) { /* eslint-disable-next-line max-len */ @@ -440,7 +406,7 @@ function checkData (data, sri, opts) { module.exports.checkStream = checkStream function checkStream (stream, sri, opts) { - opts = ssriOpts(opts) + opts = opts || Object.create(null) opts.integrity = sri sri = parse(sri, opts) if (!sri || !Object.keys(sri).length) { @@ -465,15 +431,14 @@ function checkStream (stream, sri, opts) { } module.exports.integrityStream = integrityStream -function integrityStream (opts = {}) { +function integrityStream (opts = Object.create(null)) { return new IntegrityStream(opts) } module.exports.create = createIntegrity function createIntegrity (opts) { - opts = ssriOpts(opts) - const algorithms = opts.algorithms - const optString = getOptString(opts.options) + const algorithms = opts?.algorithms || DEFAULT_ALGORITHMS + const optString = getOptString(opts?.options) const hashes = algorithms.map(crypto.createHash)