From 374304383a82c3bfa255bf9e6da4eb66ba911987 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Louren=C3=A7o?= Date: Sat, 1 Apr 2023 01:56:16 -0300 Subject: [PATCH 1/4] perf: remove spread of defaultOpts --- lib/index.js | 57 +++++++++++++++++++++------------------------------- 1 file changed, 23 insertions(+), 34 deletions(-) diff --git a/lib/index.js b/lib/index.js index 222861a..034ba4c 100644 --- a/lib/index.js +++ b/lib/index.js @@ -3,7 +3,11 @@ const crypto = require('crypto') const MiniPass = require('minipass') -const SPEC_ALGORITHMS = ['sha256', 'sha384', 'sha512'] +const SPEC_ALGORITHMS = { + 'sha256': true, + 'sha384': true, + 'sha512': true +}; // TODO: this should really be a hardcoded list of algorithms we support, // rather than [a-z0-9]. @@ -22,8 +26,6 @@ const defaultOpts = { strict: false, } -const ssriOpts = (opts = {}) => ({ ...defaultOpts, ...opts }) - const getOptString = options => !options || !options.length ? '' : `?${options.join('?')}` @@ -44,7 +46,7 @@ class IntegrityStream extends MiniPass { this[_getOptions]() // options used for calculating stream. can't be changed. - const { algorithms = defaultOpts.algorithms } = opts + const algorithms = opts && opts.algorithms || defaultOpts.algorithms this.algorithms = Array.from( new Set(algorithms.concat(this.algorithm ? [this.algorithm] : [])) ) @@ -141,8 +143,7 @@ class Hash { } constructor (hash, opts) { - opts = ssriOpts(opts) - const strict = !!opts.strict + const strict = opts && opts.strict this.source = hash.trim() // set default values so that we make V8 happy to @@ -161,7 +162,7 @@ class Hash { if (!match) { return } - if (strict && !SPEC_ALGORITHMS.some(a => a === match[1])) { + if (strict && SPEC_ALGORITHMS[match[1]] !== true) { return } this.algorithm = match[1] @@ -182,14 +183,13 @@ class Hash { } toString (opts) { - opts = ssriOpts(opts) - if (opts.strict) { + if (opts && 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[this.algorithm] === true && // 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. @@ -224,8 +224,7 @@ class Integrity { } toString (opts) { - opts = ssriOpts(opts) - let sep = opts.sep || ' ' + let sep = opts && opts.sep || ' ' if (opts.strict) { // Entries must be separated by whitespace, according to spec. sep = sep.replace(/\S+/g, ' ') @@ -238,7 +237,6 @@ class Integrity { } concat (integrity, opts) { - opts = ssriOpts(opts) const other = typeof integrity === 'string' ? integrity : stringify(integrity, opts) @@ -252,7 +250,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 +265,6 @@ class Integrity { } match (integrity, opts) { - opts = ssriOpts(opts) const other = parse(integrity, opts) if (!other) { return false @@ -286,8 +282,7 @@ class Integrity { } pickAlgorithm (opts) { - opts = ssriOpts(opts) - const pickAlgorithm = opts.pickAlgorithm + const pickAlgorithm = opts && opts.pickAlgorithm || defaultOpts.pickAlgorithm; const keys = Object.keys(this) return keys.reduce((acc, algo) => { return pickAlgorithm(acc, algo) || acc @@ -300,7 +295,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 +309,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 && opts.single) { return new Hash(integrity, opts) } const hashes = integrity.trim().split(/\s+/).reduce((acc, string) => { @@ -334,7 +328,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 +339,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 && opts.options) return parse( `${algorithm}-${ Buffer.from(hexDigest, 'hex').toString('base64') @@ -357,9 +349,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 && opts.algorithms || defaultOpts.algorithms + const optString = getOptString(opts && opts.options) return algorithms.reduce((acc, algo) => { const digest = crypto.createHash(algo).update(data).digest('base64') const hash = new Hash( @@ -382,7 +373,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 +389,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 && opts.error) { throw Object.assign( new Error('No valid integrity hashes to check against'), { code: 'EINTEGRITY', @@ -416,7 +405,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 && opts.error)) { return match } else if (typeof opts.size === 'number' && (data.length !== opts.size)) { /* eslint-disable-next-line max-len */ @@ -440,7 +430,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 +455,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 && opts.algorithms || defaultOpts.algorithms + const optString = getOptString(opts && opts.options) const hashes = algorithms.map(crypto.createHash) From 00378da653b036732eaee34bea5a9ab7cbf4d36a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Louren=C3=A7o?= Date: Sat, 1 Apr 2023 02:00:38 -0300 Subject: [PATCH 2/4] perf: faster toString for integrity --- lib/index.js | 62 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/lib/index.js b/lib/index.js index 034ba4c..042c672 100644 --- a/lib/index.js +++ b/lib/index.js @@ -210,6 +210,41 @@ class Hash { } } +function integrityHashToString(toString, sep, opts, hashes) { + if (!hashes || hashes.length === 0) + return toString; + + const toStringIsNotEmpty = toString !== ''; + + let shouldAddFirstSep = false; + let complement = ''; + + const lastIndex = hashes.length - 1 + + for (let i = 0; i < lastIndex; i++) { + const hashString = Hash.prototype.toString.call(hashes[i], opts) + + if (hashString) { + shouldAddFirstSep = true; + + complement += hashString + complement += sep + } + } + + const finalHashString = Hash.prototype.toString.call(hashes[lastIndex], opts) + + if (finalHashString) { + shouldAddFirstSep = true; + complement += finalHashString + } + + if (toStringIsNotEmpty && shouldAddFirstSep) + return toString + sep + complement; + + return toString + complement +} + class Integrity { get isIntegrity () { return true @@ -225,15 +260,30 @@ class Integrity { toString (opts) { let sep = opts && opts.sep || ' ' - if (opts.strict) { + let toString = ''; + + if (opts && opts.strict) { // Entries must be separated by whitespace, according to spec. sep = sep.replace(/\S+/g, ' ') + + if (this.sha512 && this.sha512.length > 0) { + toString = integrityHashToString(toString, sep,opts, this['sha512']) + } + + if (this.sha384 && this.sha384.length > 0) { + toString = integrityHashToString(toString, sep,opts, this['sha384']) + } + + if (this.sha256 && this.sha256.length > 0) { + toString = integrityHashToString(toString,sep, opts, this['sha256']) + } + } else { + for (const hash of Object.keys(this)) { + toString = integrityHashToString(toString, sep,opts, this[hash]) + } } - return Object.keys(this).map(k => { - return this[k].map(hash => { - return Hash.prototype.toString.call(hash, opts) - }).filter(x => x.length).join(sep) - }).filter(x => x.length).join(sep) + + return toString; } concat (integrity, opts) { From d54207b1cfc604d7d2a8a791f127d66d2f7bd382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Louren=C3=A7o?= Date: Sat, 1 Apr 2023 15:58:22 -0300 Subject: [PATCH 3/4] perf: faster stream verification --- lib/index.js | 55 ++++++++++++++++++++++++++++++++++----------------- test/check.js | 3 +++ 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/lib/index.js b/lib/index.js index 042c672..8b7c04e 100644 --- a/lib/index.js +++ b/lib/index.js @@ -40,33 +40,41 @@ class IntegrityStream extends MiniPass { constructor (opts) { super() this.size = 0 - this.opts = opts + this.opts = opts || Object.create(null) // may be overridden later, but set now for class consistency this[_getOptions]() // options used for calculating stream. can't be changed. - const algorithms = opts && opts.algorithms || defaultOpts.algorithms - this.algorithms = Array.from( - new Set(algorithms.concat(this.algorithm ? [this.algorithm] : [])) - ) + if (opts && opts.algorithms) { + const algorithms = opts.algorithms + this.algorithms = Array.from( + new Set(algorithms.concat(this.algorithm ? [this.algorithm] : [])) + ) + } else { + this.algorithms = defaultOpts.algorithms + + if (this.algorithm !== null && this.algorithm !== defaultOpts.algorithms[0]) + this.algorithms.push(this.algorithm); + } + this.hashes = this.algorithms.map(crypto.createHash) } [_getOptions] () { - const { - integrity, - size, - options, - } = { ...defaultOpts, ...this.opts } - // For verification - this.sri = integrity ? parse(integrity, this.opts) : null - this.expectedSize = size - this.goodSri = this.sri ? !!Object.keys(this.sri).length : false - this.algorithm = this.goodSri ? this.sri.pickAlgorithm(this.opts) : null + this.sri = this.opts.integrity ? parse(this.opts.integrity, this.opts) : null + this.expectedSize = this.opts.size + this.goodSri = this.sri instanceof Integrity + ? !!Object.keys(this.sri).length + : this.sri instanceof Hash + this.algorithm = this.goodSri + ? this.sri instanceof Integrity + ? this.sri.pickAlgorithm(this.opts) + : this.sri.algorithm + : null this.digests = this.goodSri ? this.sri[this.algorithm] : null - this.optString = getOptString(options) + this.optString = getOptString(this.opts.options) } on (ev, handler) { @@ -182,6 +190,17 @@ class Hash { return this.toString() } + match (integrity, opts) { + const other = parse(integrity, opts) + if (!other) { + return false + } + const algo = other instanceof Integrity + ? other.pickAlgorithm(opts) + : other + return algo.digest === this.digest ? algo : false + } + toString (opts) { if (opts && opts.strict) { // Strict mode enforces the standard as close to the foot of the @@ -433,7 +452,7 @@ function fromStream (stream, opts) { sri = s }) istream.on('end', () => resolve(sri)) - istream.on('data', () => {}) + istream.resume() }) } @@ -500,7 +519,7 @@ function checkStream (stream, sri, opts) { verified = s }) checker.on('end', () => resolve(verified)) - checker.on('data', () => {}) + checker.resume() }) } diff --git a/test/check.js b/test/check.js index f8cdc6e..6ca6cca 100644 --- a/test/check.js +++ b/test/check.js @@ -160,6 +160,9 @@ test('checkStream', t => { }) }).then(res => { t.same(res, meta, 'Accepts Hash-like SRI') + return ssri.checkStream(fileStream(), `sha512-${hash(TEST_DATA, 'sha512')}`, { single: true }) + }).then(res => { + t.same(res, meta, 'Process successfully with single option') return ssri.checkStream( fileStream(), `sha512-nope sha512-${hash(TEST_DATA, 'sha512')}` From 694f7ed19c44484463633cd9845938a09be3d1d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Louren=C3=A7o?= Date: Sat, 1 Apr 2023 16:01:18 -0300 Subject: [PATCH 4/4] refactor: use more optional chaining instead && --- lib/index.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/index.js b/lib/index.js index 8b7c04e..bffa4e4 100644 --- a/lib/index.js +++ b/lib/index.js @@ -46,7 +46,7 @@ class IntegrityStream extends MiniPass { this[_getOptions]() // options used for calculating stream. can't be changed. - if (opts && opts.algorithms) { + if (opts?.algorithms) { const algorithms = opts.algorithms this.algorithms = Array.from( new Set(algorithms.concat(this.algorithm ? [this.algorithm] : [])) @@ -151,7 +151,7 @@ class Hash { } constructor (hash, opts) { - const strict = opts && opts.strict + const strict = opts?.strict this.source = hash.trim() // set default values so that we make V8 happy to @@ -202,7 +202,7 @@ class Hash { } toString (opts) { - if (opts && opts.strict) { + if (opts?.strict) { // Strict mode enforces the standard as close to the foot of the // letter as it can. if (!( @@ -278,10 +278,10 @@ class Integrity { } toString (opts) { - let sep = opts && opts.sep || ' ' + let sep = opts?.sep || ' ' let toString = ''; - if (opts && opts.strict) { + if (opts?.strict) { // Entries must be separated by whitespace, according to spec. sep = sep.replace(/\S+/g, ' ') @@ -351,7 +351,7 @@ class Integrity { } pickAlgorithm (opts) { - const pickAlgorithm = opts && opts.pickAlgorithm || defaultOpts.pickAlgorithm; + const pickAlgorithm = opts?.pickAlgorithm || defaultOpts.pickAlgorithm; const keys = Object.keys(this) return keys.reduce((acc, algo) => { return pickAlgorithm(acc, algo) || acc @@ -378,7 +378,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 && opts.single) { + if (opts?.single) { return new Hash(integrity, opts) } const hashes = integrity.trim().split(/\s+/).reduce((acc, string) => { @@ -408,7 +408,7 @@ function stringify (obj, opts) { module.exports.fromHex = fromHex function fromHex (hexDigest, algorithm, opts) { - const optString = getOptString(opts && opts.options) + const optString = getOptString(opts?.options) return parse( `${algorithm}-${ Buffer.from(hexDigest, 'hex').toString('base64') @@ -418,8 +418,8 @@ function fromHex (hexDigest, algorithm, opts) { module.exports.fromData = fromData function fromData (data, opts) { - const algorithms = opts && opts.algorithms || defaultOpts.algorithms - const optString = getOptString(opts && opts.options) + const algorithms = opts?.algorithms || defaultOpts.algorithms + const optString = getOptString(opts?.options) return algorithms.reduce((acc, algo) => { const digest = crypto.createHash(algo).update(data).digest('base64') const hash = new Hash( @@ -460,7 +460,7 @@ module.exports.checkData = checkData function checkData (data, sri, opts) { sri = parse(sri, opts) if (!sri || !Object.keys(sri).length) { - if (opts && opts.error) { + if (opts?.error) { throw Object.assign( new Error('No valid integrity hashes to check against'), { code: 'EINTEGRITY', @@ -475,7 +475,7 @@ function checkData (data, sri, opts) { const newSri = parse({ algorithm, digest }) const match = newSri.match(sri, opts) opts = opts || Object.create(null) - if (match || !(opts && opts.error)) { + if (match || !(opts?.error)) { return match } else if (typeof opts.size === 'number' && (data.length !== opts.size)) { /* eslint-disable-next-line max-len */ @@ -530,8 +530,8 @@ function integrityStream (opts = Object.create(null)) { module.exports.create = createIntegrity function createIntegrity (opts) { - const algorithms = opts && opts.algorithms || defaultOpts.algorithms - const optString = getOptString(opts && opts.options) + const algorithms = opts?.algorithms || defaultOpts.algorithms + const optString = getOptString(opts?.options) const hashes = algorithms.map(crypto.createHash)