Comparing version 2.0.0 to 3.0.0
@@ -5,2 +5,34 @@ # Change Log | ||
<a name="3.0.0"></a> | ||
# [3.0.0](https://github.com/zkat/ssri/compare/v2.0.0...v3.0.0) (2017-04-03) | ||
### Bug Fixes | ||
* **hashes:** IntegrityMetadata -> Hash ([d04aa1f](https://github.com/zkat/ssri/commit/d04aa1f)) | ||
### Features | ||
* **check:** return IntegrityMetadata on check success ([2301e74](https://github.com/zkat/ssri/commit/2301e74)) | ||
* **fromHex:** ssri.fromHex to make it easier to generate them from hex valus ([049b89e](https://github.com/zkat/ssri/commit/049b89e)) | ||
* **hex:** utility function for getting hex version of digest ([a9f021c](https://github.com/zkat/ssri/commit/a9f021c)) | ||
* **hexDigest:** added hexDigest method to Integrity objects too ([85208ba](https://github.com/zkat/ssri/commit/85208ba)) | ||
* **integrity:** add .isIntegrity and .isIntegrityMetadata ([1b29e6f](https://github.com/zkat/ssri/commit/1b29e6f)) | ||
* **integrityStream:** new stream that can both generate and check streamed data ([fd23e1b](https://github.com/zkat/ssri/commit/fd23e1b)) | ||
* **parse:** allow parsing straight into a single IntegrityMetadata object ([c8ddf48](https://github.com/zkat/ssri/commit/c8ddf48)) | ||
* **pickAlgorithm:** Intergrity#pickAlgorithm() added ([b97a796](https://github.com/zkat/ssri/commit/b97a796)) | ||
* **size:** calculate and update stream sizes ([02ed1ad](https://github.com/zkat/ssri/commit/02ed1ad)) | ||
### BREAKING CHANGES | ||
* **hashes:** `.isIntegrityMetadata` is now `.isHash`. Also, any references to `IntegrityMetadata` now refer to `Hash`. | ||
* **integrityStream:** createCheckerStream has been removed and replaced with a general-purpose integrityStream. | ||
To convert existing createCheckerStream code, move the `sri` argument into `opts.integrity` in integrityStream. All other options should be the same. | ||
* **check:** `checkData`, `checkStream`, and `createCheckerStream` now yield a whole IntegrityMetadata instance representing the first successful hash match. | ||
<a name="2.0.0"></a> | ||
@@ -7,0 +39,0 @@ # [2.0.0](https://github.com/zkat/ssri/compare/v1.0.0...v2.0.0) (2017-03-24) |
183
index.js
@@ -13,7 +13,8 @@ 'use strict' | ||
class IntegrityMetadata { | ||
constructor (metadata, opts) { | ||
class Hash { | ||
get isHash () { return true } | ||
constructor (hash, opts) { | ||
const strict = !!(opts && opts.strict) | ||
this.source = metadata.trim() | ||
// 3.1. Integrity metadata | ||
this.source = hash.trim() | ||
// 3.1. Integrity metadata (called "Hash" by ssri) | ||
// https://w3c.github.io/webappsec-subresource-integrity/#integrity-metadata-description | ||
@@ -33,2 +34,5 @@ const match = this.source.match( | ||
} | ||
hexDigest () { | ||
return this.digest && bufFrom(this.digest, 'base64').toString('hex') | ||
} | ||
toString (opts) { | ||
@@ -63,2 +67,3 @@ if (opts && opts.strict) { | ||
class Integrity { | ||
get isIntegrity () { return true } | ||
toString (opts) { | ||
@@ -72,4 +77,4 @@ opts = opts || {} | ||
return Object.keys(this).map(k => { | ||
return this[k].map(meta => { | ||
return IntegrityMetadata.prototype.toString.call(meta, opts) | ||
return this[k].map(hash => { | ||
return Hash.prototype.toString.call(hash, opts) | ||
}).filter(x => x.length).join(sep) | ||
@@ -84,2 +89,11 @@ }).filter(x => x.length).join(sep) | ||
} | ||
hexDigest () { | ||
return parse(this, {single: true}).hexDigest() | ||
} | ||
pickAlgorithm (opts) { | ||
const pickAlgorithm = (opts && opts.pickAlgorithm) || getPrioritizedHash | ||
return Object.keys(this).reduce((acc, algo) => { | ||
return pickAlgorithm(acc, algo) || acc | ||
}) | ||
} | ||
} | ||
@@ -104,8 +118,11 @@ | ||
// https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata | ||
if (opts.single) { | ||
return new Hash(integrity, opts) | ||
} | ||
return integrity.trim().split(/\s+/).reduce((acc, string) => { | ||
const metadata = new IntegrityMetadata(string, opts) | ||
if (metadata.algorithm && metadata.digest) { | ||
const algo = metadata.algorithm | ||
const hash = new Hash(string, opts) | ||
if (hash.algorithm && hash.digest) { | ||
const algo = hash.algorithm | ||
if (!acc[algo]) { acc[algo] = [] } | ||
acc[algo].push(metadata) | ||
acc[algo].push(hash) | ||
} | ||
@@ -119,3 +136,3 @@ return acc | ||
if (obj.algorithm && obj.digest) { | ||
return IntegrityMetadata.prototype.toString.call(obj, opts) | ||
return Hash.prototype.toString.call(obj, opts) | ||
} else if (typeof obj === 'string') { | ||
@@ -128,2 +145,14 @@ return stringify(parse(obj, opts), opts) | ||
module.exports.fromHex = fromHex | ||
function fromHex (hexDigest, algorithm, opts) { | ||
const optString = (opts && opts.options && opts.options.length) | ||
? `?${opts.options.join('?')}` | ||
: '' | ||
return parse( | ||
`${algorithm}-${ | ||
bufFrom(hexDigest, 'hex').toString('base64') | ||
}${optString}`, opts | ||
) | ||
} | ||
module.exports.fromData = fromData | ||
@@ -138,10 +167,10 @@ function fromData (data, opts) { | ||
const digest = crypto.createHash(algo).update(data).digest('base64') | ||
const meta = new IntegrityMetadata( | ||
const hash = new Hash( | ||
`${algo}-${digest}${optString}`, | ||
opts | ||
) | ||
if (meta.algorithm && meta.digest) { | ||
const algo = meta.algorithm | ||
if (hash.algorithm && hash.digest) { | ||
const algo = hash.algorithm | ||
if (!acc[algo]) { acc[algo] = [] } | ||
acc[algo].push(meta) | ||
acc[algo].push(hash) | ||
} | ||
@@ -155,27 +184,12 @@ return acc | ||
opts = opts || {} | ||
const algorithms = opts.algorithms || ['sha512'] | ||
const optString = opts.options && opts.options.length | ||
? `?${opts.options.join('?')}` | ||
: '' | ||
const P = opts.promise || Promise | ||
const P = opts.Promise || Promise | ||
const istream = integrityStream(opts) | ||
return new P((resolve, reject) => { | ||
const hashes = algorithms.map(algo => crypto.createHash(algo)) | ||
stream.on('data', d => hashes.forEach(hash => hash.update(d))) | ||
stream.pipe(istream) | ||
stream.on('error', reject) | ||
stream.on('end', () => { | ||
resolve(algorithms.reduce((acc, algo, i) => { | ||
const hash = hashes[i] | ||
const digest = hash.digest('base64') | ||
const meta = new IntegrityMetadata( | ||
`${algo}-${digest}${optString}`, | ||
opts | ||
) | ||
if (meta.algorithm && meta.digest) { | ||
const algo = meta.algorithm | ||
if (!acc[algo]) { acc[algo] = [] } | ||
acc[algo].push(meta) | ||
} | ||
return acc | ||
}, new Integrity())) | ||
}) | ||
istream.on('error', reject) | ||
let sri | ||
istream.on('integrity', s => { sri = s }) | ||
istream.on('end', () => resolve(sri)) | ||
istream.on('data', () => {}) | ||
}) | ||
@@ -188,9 +202,6 @@ } | ||
sri = parse(sri, opts) | ||
const pickAlgorithm = opts.pickAlgorithm || getPrioritizedHash | ||
const algorithm = Object.keys(sri).reduce((acc, algo) => { | ||
return pickAlgorithm(acc, algo) || acc | ||
}) | ||
const digests = sri[algorithm].map(m => m.digest) | ||
const algorithm = sri.pickAlgorithm(opts) | ||
const digests = sri[algorithm] | ||
const digest = crypto.createHash(algorithm).update(data).digest('base64') | ||
return digests.some(d => d === digest) && algorithm | ||
return digests.find(hash => hash.digest === digest) || false | ||
} | ||
@@ -202,3 +213,8 @@ | ||
const P = opts.Promise || Promise | ||
const checker = createCheckerStream(sri, opts) | ||
const checker = integrityStream({ | ||
integrity: sri, | ||
size: opts.size, | ||
strict: opts.strict, | ||
pickAlgorithm: opts.pickAlgorithm | ||
}) | ||
return new P((resolve, reject) => { | ||
@@ -208,36 +224,63 @@ stream.pipe(checker) | ||
checker.on('error', reject) | ||
checker.on('verified', algo => { | ||
resolve(algo) | ||
}) | ||
let sri | ||
checker.on('verified', s => { sri = s }) | ||
checker.on('end', () => resolve(sri)) | ||
checker.on('data', () => {}) | ||
}) | ||
} | ||
module.exports.createCheckerStream = createCheckerStream | ||
function createCheckerStream (sri, opts) { | ||
module.exports.integrityStream = integrityStream | ||
function integrityStream (opts) { | ||
opts = opts || {} | ||
sri = parse(sri, opts) | ||
const pickAlgorithm = opts.pickAlgorithm || getPrioritizedHash | ||
const algorithm = Object.keys(sri).reduce((acc, algo) => { | ||
return pickAlgorithm(acc, algo) || acc | ||
}) | ||
const digests = sri[algorithm].map(m => m.digest) | ||
const hash = crypto.createHash(algorithm) | ||
// For verification | ||
const sri = opts.integrity && parse(opts.integrity, opts) | ||
const algorithm = sri && sri.pickAlgorithm(opts) | ||
const digests = sri && sri[algorithm] | ||
// Calculating stream | ||
const algorithms = opts.algorithms || [algorithm || 'sha512'] | ||
const hashes = algorithms.map(crypto.createHash) | ||
let streamSize = 0 | ||
const stream = new Transform({ | ||
transform: function (chunk, enc, cb) { | ||
hash.update(chunk, enc) | ||
transform (chunk, enc, cb) { | ||
streamSize += chunk.length | ||
hashes.forEach(h => h.update(chunk, enc)) | ||
cb(null, chunk, enc) | ||
}, | ||
flush: function (cb) { | ||
const digest = hash.digest('base64') | ||
if (digests.some(d => d === digest)) { | ||
stream.emit('verified', algorithm) | ||
return cb() | ||
} else { | ||
const err = new Error(`${algorithm} integrity checksum failed`) | ||
flush (done) { | ||
const optString = (opts.options && opts.options.length) | ||
? `?${opts.options.join('?')}` | ||
: '' | ||
const newSri = parse(hashes.map((h, i) => { | ||
return `${algorithms[i]}-${h.digest('base64')}${optString}` | ||
}).join(' '), opts) | ||
const match = ( | ||
// Integrity verification mode | ||
opts.integrity && | ||
digests.find(hash => { | ||
return newSri[algorithm].find(newhash => { | ||
return hash.digest === newhash.digest | ||
}) | ||
}) | ||
) | ||
if (typeof opts.size === 'number' && streamSize !== opts.size) { | ||
const err = new Error(`stream size mismatch when checking ${sri}.\n Wanted: ${opts.size}\n Found: ${streamSize}`) | ||
err.code = 'EBADSIZE' | ||
err.found = streamSize | ||
err.expected = opts.size | ||
err.sri = sri | ||
stream.emit('error', err) | ||
} else if (opts.integrity && !match) { | ||
const err = new Error(`${sri} integrity checksum failed when using ${algorithm}`) | ||
err.code = 'EBADCHECKSUM' | ||
err.found = digest | ||
err.found = newSri | ||
err.expected = digests | ||
err.algorithm = algorithm | ||
return cb(err) | ||
err.sri = sri | ||
stream.emit('error', err) | ||
} else { | ||
stream.emit('size', streamSize) | ||
stream.emit('integrity', newSri) | ||
match && stream.emit('verified', match) | ||
} | ||
done() | ||
} | ||
@@ -257,1 +300,5 @@ }) | ||
} | ||
function bufFrom (data, enc) { | ||
return Buffer.from ? Buffer.from(data, enc) : new Buffer(data, enc) | ||
} |
{ | ||
"name": "ssri", | ||
"version": "2.0.0", | ||
"version": "3.0.0", | ||
"description": "Standard Subresource Integrity library -- parses, serializes, generates, and verifies integrity metadata according to the SRI spec.", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
156
README.md
@@ -23,3 +23,6 @@ # ssri [![npm version](https://img.shields.io/npm/v/ssri.svg)](https://npm.im/ssri) [![license](https://img.shields.io/npm/l/ssri.svg)](https://npm.im/ssri) [![Travis](https://img.shields.io/travis/zkat/ssri.svg)](https://travis-ci.org/zkat/ssri) [![AppVeyor](https://ci.appveyor.com/api/projects/status/github/zkat/ssri?svg=true)](https://ci.appveyor.com/project/zkat/ssri) [![Coverage Status](https://coveralls.io/repos/github/zkat/ssri/badge.svg?branch=latest)](https://coveralls.io/github/zkat/ssri?branch=latest) | ||
* [`Integrity#toString`](#integrity-to-string) | ||
* [`Integrity#pickAlgorithm`](#integrity-pick-algorithm) | ||
* [`Integrity#hexDigest`](#integrity-hex-digest) | ||
* Integrity Generation | ||
* [`fromHex`](#from-hex) | ||
* [`fromData`](#from-data) | ||
@@ -30,3 +33,3 @@ * [`fromStream`](#from-stream) | ||
* [`checkStream`](#check-stream) | ||
* [`createCheckerStream`](#create-checker-stream) | ||
* [`integrityStream`](#integrity-stream) | ||
@@ -64,3 +67,3 @@ ### Example | ||
* Multiple entries for the same algorithm. | ||
* Object-based integrity metadata manipulation. | ||
* Object-based integrity hash manipulation. | ||
* Small footprint: no dependencies, concise implementation. | ||
@@ -83,5 +86,5 @@ * Full test coverage. | ||
Parses `sri` into an `Integrity` data structure. `sri` can be an integrity | ||
string, an `IntegrityMetadata`-like with `digest` and `algorithm` fields and an | ||
optional `options` field, or an `Integrity`-like object. The resulting object | ||
will be an `Integrity` instance that has this shape: | ||
string, an `Hash`-like with `digest` and `algorithm` fields and an optional | ||
`options` field, or an `Integrity`-like object. The resulting object will be an | ||
`Integrity` instance that has this shape: | ||
@@ -98,2 +101,6 @@ ```javascript | ||
If `opts.single` is truthy, a single `Hash` object will be returned. That is, a | ||
single object that looks like `{algorithm, digest, options}`, as opposed to a | ||
larger object with multiple of these. | ||
If `opts.strict` is truthy, the resulting object will be filtered such that | ||
@@ -117,3 +124,3 @@ it strictly follows the Subresource Integrity spec, throwing away any entries | ||
except it can be used on _any_ object that [`parse`](#parse) can handle -- that | ||
is, a string, an `IntegrityMetadata`-like, or an `Integrity`-like. | ||
is, a string, an `Hash`-like, or an `Integrity`-like. | ||
@@ -134,3 +141,3 @@ The `opts.sep` option defines the string to use when joining multiple entries | ||
// IntegrityMetadata-like: only a single entry. | ||
// Hash-like: only a single entry. | ||
ssri.stringify({ | ||
@@ -160,4 +167,4 @@ algorithm: 'sha512', | ||
Concatenates an `Integrity` object with another IntegrityLike, or a string | ||
representing integrity metadata. | ||
Concatenates an `Integrity` object with another IntegrityLike, or an integrity | ||
string. | ||
@@ -187,3 +194,3 @@ This is functionally equivalent to concatenating the string format of both | ||
Returns the string representation of an `Integrity` object. All metadata entries | ||
Returns the string representation of an `Integrity` object. All hash entries | ||
will be concatenated in the string by `opts.sep`, which defaults to `' '`. | ||
@@ -205,2 +212,58 @@ | ||
#### <a name="integrity-pick-algorithm"></a> `> Integrity#pickAlgorithm([opts]) -> String` | ||
Returns the "best" algorithm from those available in the integrity object. | ||
If `opts.pickAlgorithm` is provided, it will be passed two algorithms as | ||
arguments. ssri will prioritize whichever of the two algorithms is returned by | ||
this function. Note that the function may be called multiple times, and it | ||
**must** return one of the two algorithms provided. By default, ssri will make | ||
a best-effort to pick the strongest/most reliable of the given algorithms. It | ||
may intentionally deprioritize algorithms with known vulnerabilities. | ||
##### Example | ||
```javascript | ||
ssri.parse('sha1-WEakDigEST sha512-yzd8ELD1piyANiWnmdnpCL5F52f10UfUdEkHywVZeqTt0ymgrxR63Qz0GB7TKPoeeZQmWCaz7T1').pickAlgorithm() // sha512 | ||
``` | ||
#### <a name="integrity-hex-digest"></a> `> Integrity#hexDigest() -> String` | ||
`Integrity` is assumed to be either a single-hash `Integrity` instance, or a | ||
`Hash` instance. Returns its `digest`, converted to a hex representation of the | ||
base64 data. | ||
##### Example | ||
```javascript | ||
ssri.parse('sha1-deadbeef').hexDigest() // '75e69d6de79f' | ||
``` | ||
#### <a name="from-hex"></a> `> ssri.fromHex(hexDigest, algorithm, [opts]) -> Integrity` | ||
Creates an `Integrity` object with a single entry, based on a hex-formatted | ||
hash. This is a utility function to help convert existing shasums to the | ||
Integrity format, and is roughly equivalent to something like: | ||
```javascript | ||
algorithm + '-' + Buffer.from(hexDigest, 'hex').toString('base64') | ||
``` | ||
`opts.options` may optionally be passed in: it must be an array of option | ||
strings that will be added to all generated integrity hashes generated by | ||
`fromData`. This is a loosely-specified feature of SRIs, and currently has no | ||
specified semantics besides being `?`-separated. Use at your own risk, and | ||
probably avoid if your integrity strings are meant to be used with browsers. | ||
If `opts.strict` is true, the integrity object will be created using strict | ||
parsing rules. See [`ssri.parse`](#parse). | ||
If `opts.single` is true, a single `Hash` object will be returned. | ||
##### Example | ||
```javascript | ||
ssri.fromHex('75e69d6de79f', 'sha1').toString() // 'sha1-deadbeef' | ||
``` | ||
#### <a name="from-data"></a> `> ssri.fromData(data, [opts]) -> Integrity` | ||
@@ -211,3 +274,3 @@ | ||
`opts.algorithms` determines which algorithms to generate metadata for. All | ||
`opts.algorithms` determines which algorithms to generate hashes for. All | ||
results will be included in a single `Integrity` object. The default value for | ||
@@ -218,3 +281,3 @@ `opts.algorithms` is `['sha512']`. All algorithm strings must be hashes listed | ||
`opts.options` may optionally be passed in: it must be an array of option | ||
strings that will be added to all generated integrity metadata generated by | ||
strings that will be added to all generated integrity hashes generated by | ||
`fromData`. This is a loosely-specified feature of SRIs, and currently has no | ||
@@ -264,3 +327,3 @@ specified semantics besides being `?`-separated. Use at your own risk, and | ||
#### <a name="check-data"></a> `> ssri.checkData(data, sri, [opts]) -> Algorithm|false` | ||
#### <a name="check-data"></a> `> ssri.checkData(data, sri, [opts]) -> Hash|false` | ||
@@ -274,8 +337,5 @@ Verifies `data` integrity against an `sri` argument. `data` may be either a | ||
If `opts.pickAlgorithm` is provided, it will be passed two algorithms as | ||
arguments. ssri will prioritize whichever of the two algorithms is returned by | ||
this function. Note that the function may be called multiple times, and it | ||
**must** return one of the two algorithms provided. By default, ssri will make | ||
a best-effort to pick the strongest/most reliable of the given algorithms. It | ||
may intentionally deprioritize algorithms with known vulnerabilities. | ||
If `opts.pickAlgorithm` is provided, it will be used by | ||
[`Integrity#pickAlgorithm`](#integrity-pick-algorithm) when deciding which of | ||
the available digests to match against. | ||
@@ -291,3 +351,3 @@ ##### Example | ||
#### <a name="check-stream"></a> `> ssri.checkStream(stream, sri, [opts]) -> Promise<Algorithm>` | ||
#### <a name="check-stream"></a> `> ssri.checkStream(stream, sri, [opts]) -> Promise<Hash>` | ||
@@ -298,5 +358,5 @@ Verifies the contents of `stream` against an `sri` argument. `stream` will be | ||
`checkStream` will return a Promise that either resolves to the string name of | ||
the algorithm that verification was done with, or, if the verification fails or | ||
an error happens with `stream`, the Promise will be rejected. | ||
`checkStream` will return a Promise that either resolves to the | ||
`Hash` that succeeded verification, or, if the verification fails | ||
or an error happens with `stream`, the Promise will be rejected. | ||
@@ -306,9 +366,10 @@ If the Promise is rejected because verification failed, the returned error will | ||
If `opts.pickAlgorithm` is provided, it will be passed two algorithms as | ||
arguments. ssri will prioritize whichever of the two algorithms is returned by | ||
this function. Note that the function may be called multiple times, and it | ||
**must** return one of the two algorithms provided. By default, ssri will make | ||
a best-effort to pick the strongest/most reliable of the given algorithms. It | ||
may intentionally deprioritize algorithms with known vulnerabilities. | ||
If `opts.size` is given, it will be matched against the stream size. An error | ||
with `err.code` `EBADSIZE` will be returned by a rejection if the expected size | ||
and actual size fail to match. | ||
If `opts.pickAlgorithm` is provided, it will be used by | ||
[`Integrity#pickAlgorithm`](#integrity-pick-algorithm) when deciding which of | ||
the available digests to match against. | ||
##### Example | ||
@@ -322,3 +383,8 @@ | ||
integrity | ||
) // -> Promise<'sha512'> | ||
) | ||
// -> | ||
// Promise<{ | ||
// algorithm: 'sha512', | ||
// digest: 'sha512-yzd8ELD1piyANiWnmdnpCL5F52f10UfUdEkHywVZeqTt0ymgrxR63Qz0GB7TKPoeeZQmWCaz7T1' | ||
// }> | ||
@@ -328,3 +394,3 @@ ssri.checkStream( | ||
'sha256-l981iLWj8kurw4UbNy8Lpxqdzd7UOxS50Glhv8FwfZ0' | ||
) // -> Promise<'sha256'> | ||
) // -> Promise<Hash> | ||
@@ -334,14 +400,28 @@ ssri.checkStream( | ||
'sha1-BaDDigEST' | ||
) // -> Promise<Error<EBADCHECKSUM>> | ||
) // -> Promise<Error<{code: 'EBADCHECKSUM'}>> | ||
``` | ||
#### <a name="create-checker-stream"></a> `> createCheckerStream(sri, [opts]) -> CheckerStream` | ||
#### <a name="integrity-stream"></a> `> integrityStream(sri, [opts]) -> IntegrityStream` | ||
Returns a `Through` stream that data can be piped through in order to check it | ||
against `sri`. `sri` can be any subresource integrity representation that | ||
[`ssri.parse`](#parse) can handle. | ||
Returns a `Transform` stream that data can be piped through in order to generate | ||
and optionally check data integrity for piped data. When the stream completes | ||
successfully, it emits `size` and `integrity` events, containing the total | ||
number of bytes processed and a calculated `Integrity` instance based on stream | ||
data, respectively. | ||
If verification fails, the returned stream will error with an `EBADCHECKSUM` | ||
error code. | ||
If `opts.algorithms` is passed in, the listed algorithms will be calculated when | ||
generating the final `Integrity` instance. The default is `['sha512']`. | ||
If `opts.single` is passed in, a single `Hash` instance will be returned. | ||
If `opts.integrity` is passed in, it should be an `integrity` value understood | ||
by [`parse`](#parse) that the stream will check the data against. If | ||
verification succeeds, the integrity stream will emit a `verified` event whose | ||
value is a single `Hash` object that is the one that succeeded verification. If | ||
verification fails, the stream will error with an `EBADCHECKSUM` error code. | ||
If `opts.size` is given, it will be matched against the stream size. An error | ||
with `err.code` `EBADSIZE` will be emitted by the stream if the expected size | ||
and actual size fail to match. | ||
If `opts.pickAlgorithm` is provided, it will be passed two algorithms as | ||
@@ -348,0 +428,0 @@ arguments. ssri will prioritize whichever of the two algorithms is returned by |
30421
273
421