diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 952c140..f93e26d 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "10.0.2" + ".": "10.0.3" } diff --git a/CHANGELOG.md b/CHANGELOG.md index e55a91a..9fa88bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [10.0.3](https://github.com/npm/ssri/compare/v10.0.2...v10.0.3) (2023-04-11) + +### Bug Fixes + +* [`7fef846`](https://github.com/npm/ssri/commit/7fef8463d4218684302d0a832dd4364e4d605d79) [#79](https://github.com/npm/ssri/pull/79) optimize adding this.algorithm to algorithms list (@wraithgar) +* [`d90f674`](https://github.com/npm/ssri/commit/d90f674cd515e205ab00f8a264e258cd8b2bac0b) [#79](https://github.com/npm/ssri/pull/79) prevent DEFAULT_ALGORITHM mutation (@wraithgar) +* [`4e94d15`](https://github.com/npm/ssri/commit/4e94d15dd9ec5c4b1f5d9399acef11a63b322336) [#79](https://github.com/npm/ssri/pull/79) Integrity#match prioritizes overlapping hashes (@wraithgar) +* [`dce3dab`](https://github.com/npm/ssri/commit/dce3dab846124c1ef91cb7c646d98dbb08482055) [#79](https://github.com/npm/ssri/pull/79) faster stream verification (@H4ad) + ## [10.0.2](https://github.com/npm/ssri/compare/v10.0.1...v10.0.2) (2023-04-03) ### Bug Fixes diff --git a/lib/index.js b/lib/index.js index e142431..7db8dcf 100644 --- a/lib/index.js +++ b/lib/index.js @@ -29,10 +29,15 @@ class IntegrityStream extends MiniPass { this.#getOptions() // options used for calculating stream. can't be changed. - const algorithms = opts?.algorithms || DEFAULT_ALGORITHMS - this.algorithms = Array.from( - new Set(algorithms.concat(this.algorithm ? [this.algorithm] : [])) - ) + if (opts?.algorithms) { + this.algorithms = [...opts.algorithms] + } else { + this.algorithms = [...DEFAULT_ALGORITHMS] + } + if (this.algorithm !== null && !this.algorithms.includes(this.algorithm)) { + this.algorithms.push(this.algorithm) + } + this.hashes = this.algorithms.map(crypto.createHash) } @@ -40,8 +45,17 @@ class IntegrityStream extends MiniPass { // For verification 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 + + if (!this.sri) { + this.algorithm = null + } else if (this.sri.isHash) { + this.goodSri = true + this.algorithm = this.sri.algorithm + } else { + this.goodSri = !this.sri.isEmpty() + this.algorithm = this.sri.pickAlgorithm(this.opts) + } + this.digests = this.goodSri ? this.sri[this.algorithm] : null this.optString = getOptString(this.opts?.options) } @@ -159,6 +173,29 @@ class Hash { return this.toString() } + match (integrity, opts) { + const other = parse(integrity, opts) + if (!other) { + return false + } + if (other.isIntegrity) { + const algo = other.pickAlgorithm(opts, [this.algorithm]) + + if (!algo) { + return false + } + + const foundHash = other[algo].find(hash => hash.digest === this.digest) + + if (foundHash) { + return foundHash + } + + return false + } + return other.digest === this.digest ? other : false + } + toString (opts) { if (opts?.strict) { // Strict mode enforces the standard as close to the foot of the @@ -285,8 +322,9 @@ class Integrity { if (!other) { return false } - const algo = other.pickAlgorithm(opts) + const algo = other.pickAlgorithm(opts, Object.keys(this)) return ( + !!algo && this[algo] && other[algo] && this[algo].find(hash => @@ -297,12 +335,22 @@ class Integrity { ) || false } - pickAlgorithm (opts) { + // Pick the highest priority algorithm present, optionally also limited to a + // set of hashes found in another integrity. When limiting it may return + // nothing. + pickAlgorithm (opts, hashes) { const pickAlgorithm = opts?.pickAlgorithm || getPrioritizedHash - const keys = Object.keys(this) - return keys.reduce((acc, algo) => { - return pickAlgorithm(acc, algo) || acc + const keys = Object.keys(this).filter(k => { + if (hashes?.length) { + return hashes.includes(k) + } + return true }) + if (keys.length) { + return keys.reduce((acc, algo) => pickAlgorithm(acc, algo) || acc) + } + // no intersection between this and hashes, + return null } } @@ -365,7 +413,7 @@ function fromHex (hexDigest, algorithm, opts) { module.exports.fromData = fromData function fromData (data, opts) { - const algorithms = opts?.algorithms || DEFAULT_ALGORITHMS + 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') @@ -399,7 +447,7 @@ function fromStream (stream, opts) { sri = s }) istream.on('end', () => resolve(sri)) - istream.on('data', () => {}) + istream.resume() }) } @@ -466,7 +514,7 @@ function checkStream (stream, sri, opts) { verified = s }) checker.on('end', () => resolve(verified)) - checker.on('data', () => {}) + checker.resume() }) } @@ -477,7 +525,7 @@ function integrityStream (opts = Object.create(null)) { module.exports.create = createIntegrity function createIntegrity (opts) { - const algorithms = opts?.algorithms || DEFAULT_ALGORITHMS + const algorithms = opts?.algorithms || [...DEFAULT_ALGORITHMS] const optString = getOptString(opts?.options) const hashes = algorithms.map(crypto.createHash) @@ -512,7 +560,7 @@ function createIntegrity (opts) { } } -const NODE_HASHES = new Set(crypto.getHashes()) +const NODE_HASHES = crypto.getHashes() // This is a Best Effortâ„¢ at a reasonable priority for hash algos const DEFAULT_PRIORITY = [ @@ -522,7 +570,7 @@ const DEFAULT_PRIORITY = [ 'sha3', 'sha3-256', 'sha3-384', 'sha3-512', 'sha3_256', 'sha3_384', 'sha3_512', -].filter(algo => NODE_HASHES.has(algo)) +].filter(algo => NODE_HASHES.includes(algo)) function getPrioritizedHash (algo1, algo2) { /* eslint-disable-next-line max-len */ diff --git a/package.json b/package.json index 4d5963e..b41400c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ssri", - "version": "10.0.2", + "version": "10.0.3", "description": "Standard Subresource Integrity library -- parses, serializes, generates, and verifies integrity metadata according to the SRI spec.", "main": "lib/index.js", "files": [ 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')}` diff --git a/test/match.js b/test/match.js new file mode 100644 index 0000000..5e313a4 --- /dev/null +++ b/test/match.js @@ -0,0 +1,56 @@ +'use strict' + +const crypto = require('crypto') +const fs = require('fs') +const test = require('tap').test + +const ssri = require('..') + +const TEST_DATA = fs.readFileSync(__filename) + +function hash (data, algorithm) { + return crypto.createHash(algorithm).update(data).digest('base64') +} + +test('hashes should match when valid', t => { + const integrity = `sha512-${hash(TEST_DATA, 'sha512')}` + const otherIntegrity = `sha512-${hash('mismatch', 'sha512')}` + const parsed = ssri.parse(integrity, { single: true }) + t.same( + parsed.match(integrity, { single: true }), + parsed, + 'should return the same algo when digest is equal (single option)' + ) + t.same( + parsed.match('sha-233', { single: true }), + false, + 'invalid integrity should not match (single option)' + ) + t.same( + parsed.match(null, { single: true }), + false, + 'null integrity just returns false (single option)' + ) + + t.same( + parsed.match(integrity), + parsed, + 'should return the same algo when digest is equal' + ) + t.same( + parsed.match('sha-233'), + false, + 'invalid integrity should not match' + ) + t.same( + parsed.match(null), + false, + 'null integrity just returns false' + ) + t.same( + parsed.match(otherIntegrity), + false, + 'should not match with a totally different integrity' + ) + t.end() +})