Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrity#match bugfix and other optimizations #79

Merged
merged 4 commits into from
Apr 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 65 additions & 17 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,33 @@ 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)
}

#getOptions () {
// 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)
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 =>
Expand All @@ -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
}
}

Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -399,7 +447,7 @@ function fromStream (stream, opts) {
sri = s
})
istream.on('end', () => resolve(sri))
istream.on('data', () => {})
istream.resume()
})
}

Expand Down Expand Up @@ -466,7 +514,7 @@ function checkStream (stream, sri, opts) {
verified = s
})
checker.on('end', () => resolve(verified))
checker.on('data', () => {})
checker.resume()
})
}

Expand All @@ -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)
Expand Down Expand Up @@ -512,7 +560,7 @@ function createIntegrity (opts) {
}
}

const NODE_HASHES = new Set(crypto.getHashes())
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sets are slower to instantiate and only faster on lookups when we get into the thousands of entries. On my machine this has 52 entries.

const NODE_HASHES = crypto.getHashes()

// This is a Best Effort™ at a reasonable priority for hash algos
const DEFAULT_PRIORITY = [
Expand All @@ -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 */
Expand Down
3 changes: 3 additions & 0 deletions test/check.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')}`
Expand Down
56 changes: 56 additions & 0 deletions test/match.js
Original file line number Diff line number Diff line change
@@ -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()
})