avancement planning
This commit is contained in:
+33
-63
@@ -5,14 +5,21 @@ const { Minipass } = require('minipass')
|
||||
|
||||
const SPEC_ALGORITHMS = ['sha512', 'sha384', 'sha256']
|
||||
const DEFAULT_ALGORITHMS = ['sha512']
|
||||
const NODE_HASHES = crypto.getHashes()
|
||||
|
||||
// TODO: this should really be a hardcoded list of algorithms we support,
|
||||
// rather than [a-z0-9].
|
||||
// TODO: this should really be a hardcoded list of algorithms we support, rather than [a-z0-9].
|
||||
const BASE64_REGEX = /^[a-z0-9+/]+(?:=?=?)$/i
|
||||
const SRI_REGEX = /^([a-z0-9]+)-([^?]+)([?\S*]*)$/
|
||||
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]+$/
|
||||
|
||||
// This is a Best Effort™ at a reasonable priority for hash algos
|
||||
const DEFAULT_PRIORITY = [
|
||||
'md5', 'whirlpool', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512',
|
||||
// TODO - it's unclear _which_ of these Node will actually use as its name for the algorithm, so we guesswork it based on the OpenSSL names.
|
||||
'sha3', 'sha3-256', 'sha3-384', 'sha3-512', 'sha3_256', 'sha3_384', 'sha3_512',
|
||||
].filter(algo => NODE_HASHES.includes(algo))
|
||||
|
||||
const getOptString = options => options?.length ? `?${options.join('?')}` : ''
|
||||
|
||||
class IntegrityStream extends Minipass {
|
||||
@@ -99,7 +106,6 @@ class IntegrityStream extends Minipass {
|
||||
// Integrity verification mode
|
||||
const match = this.goodSri && newSri.match(this.sri, this.opts)
|
||||
if (typeof this.expectedSize === 'number' && this.size !== this.expectedSize) {
|
||||
/* eslint-disable-next-line max-len */
|
||||
const err = new Error(`stream size mismatch when checking ${this.sri}.\n Wanted: ${this.expectedSize}\n Found: ${this.size}`)
|
||||
err.code = 'EBADSIZE'
|
||||
err.found = this.size
|
||||
@@ -107,7 +113,6 @@ class IntegrityStream extends Minipass {
|
||||
err.sri = this.sri
|
||||
this.emit('error', err)
|
||||
} else if (this.sri && !match) {
|
||||
/* eslint-disable-next-line max-len */
|
||||
const err = new Error(`${this.sri} integrity checksum failed when using ${this.algorithm}: wanted ${this.digests} but got ${newSri}. (${this.size} bytes)`)
|
||||
err.code = 'EINTEGRITY'
|
||||
err.found = newSri
|
||||
@@ -137,8 +142,7 @@ class Hash {
|
||||
const strict = opts?.strict
|
||||
this.source = hash.trim()
|
||||
|
||||
// set default values so that we make V8 happy to
|
||||
// always see a familiar object template.
|
||||
// set default values so that we make V8 happy to always see a familiar object template.
|
||||
this.digest = ''
|
||||
this.algorithm = ''
|
||||
this.options = []
|
||||
@@ -156,6 +160,9 @@ class Hash {
|
||||
if (strict && !SPEC_ALGORITHMS.includes(match[1])) {
|
||||
return
|
||||
}
|
||||
if (!NODE_HASHES.includes(match[1])) {
|
||||
return
|
||||
}
|
||||
this.algorithm = match[1]
|
||||
this.digest = match[2]
|
||||
|
||||
@@ -198,15 +205,12 @@ class Hash {
|
||||
|
||||
toString (opts) {
|
||||
if (opts?.strict) {
|
||||
// Strict mode enforces the standard as close to the foot of the
|
||||
// letter as it can.
|
||||
// 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.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.
|
||||
// Usually, if someone insists on using a "different" base64, we leave it as-is, since there are multiple standards, and the specified is not a URL-safe variant.
|
||||
// https://www.w3.org/TR/CSP2/#base64_value
|
||||
this.digest.match(BASE64_REGEX) &&
|
||||
// Option syntax is strictly visual chars.
|
||||
@@ -300,8 +304,7 @@ class Integrity {
|
||||
return parse(this, { single: true }).hexDigest()
|
||||
}
|
||||
|
||||
// add additional hashes to an integrity value, but prevent
|
||||
// *changing* an existing integrity hash.
|
||||
// add additional hashes to an integrity value, but prevent *changing* an existing integrity hash.
|
||||
merge (integrity, opts) {
|
||||
const other = parse(integrity, opts)
|
||||
for (const algo in other) {
|
||||
@@ -323,33 +326,25 @@ class Integrity {
|
||||
return false
|
||||
}
|
||||
const algo = other.pickAlgorithm(opts, Object.keys(this))
|
||||
return (
|
||||
!!algo &&
|
||||
this[algo] &&
|
||||
other[algo] &&
|
||||
this[algo].find(hash =>
|
||||
other[algo].find(otherhash =>
|
||||
hash.digest === otherhash.digest
|
||||
)
|
||||
return !!algo && this[algo].find(hash =>
|
||||
other[algo].find(otherhash =>
|
||||
hash.digest === otherhash.digest
|
||||
)
|
||||
) || false
|
||||
}
|
||||
|
||||
// Pick the highest priority algorithm present, optionally also limited to a
|
||||
// set of hashes found in another integrity. When limiting it may return
|
||||
// nothing.
|
||||
// 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).filter(k => {
|
||||
if (hashes?.length) {
|
||||
return hashes.includes(k)
|
||||
}
|
||||
return true
|
||||
})
|
||||
let keys = Object.keys(this)
|
||||
if (hashes?.length) {
|
||||
keys = keys.filter(k => hashes.includes(k))
|
||||
}
|
||||
if (keys.length) {
|
||||
return keys.reduce((acc, algo) => pickAlgorithm(acc, algo) || acc)
|
||||
}
|
||||
// no intersection between this and hashes,
|
||||
// no intersection between this and hashes
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -380,7 +375,7 @@ function _parse (integrity, opts) {
|
||||
const hash = new Hash(string, opts)
|
||||
if (hash.algorithm && hash.digest) {
|
||||
const algo = hash.algorithm
|
||||
if (!acc[algo]) {
|
||||
if (!Object.keys(acc).includes(algo)) {
|
||||
acc[algo] = []
|
||||
}
|
||||
acc[algo].push(hash)
|
||||
@@ -421,9 +416,7 @@ function fromData (data, opts) {
|
||||
`${algo}-${digest}${optString}`,
|
||||
opts
|
||||
)
|
||||
/* istanbul ignore else - it would be VERY strange if the string we
|
||||
* just calculated with an algo did not have an algo or digest.
|
||||
*/
|
||||
// istanbul ignore else - it would be VERY strange if the string we just calculated with an algo did not have an algo or digest.
|
||||
if (hash.algorithm && hash.digest) {
|
||||
const hashAlgo = hash.algorithm
|
||||
if (!acc[hashAlgo]) {
|
||||
@@ -473,7 +466,6 @@ function checkData (data, sri, opts) {
|
||||
if (match || !(opts.error)) {
|
||||
return match
|
||||
} else if (typeof opts.size === 'number' && (data.length !== opts.size)) {
|
||||
/* eslint-disable-next-line max-len */
|
||||
const err = new Error(`data size mismatch when checking ${sri}.\n Wanted: ${opts.size}\n Found: ${data.length}`)
|
||||
err.code = 'EBADSIZE'
|
||||
err.found = data.length
|
||||
@@ -481,7 +473,6 @@ function checkData (data, sri, opts) {
|
||||
err.sri = sri
|
||||
throw err
|
||||
} else {
|
||||
/* eslint-disable-next-line max-len */
|
||||
const err = new Error(`Integrity checksum failed when using ${algorithm}: Wanted ${sri}, but got ${newSri}. (${data.length} bytes)`)
|
||||
err.code = 'EINTEGRITY'
|
||||
err.found = newSri
|
||||
@@ -538,20 +529,11 @@ function createIntegrity (opts) {
|
||||
digest: function () {
|
||||
const integrity = algorithms.reduce((acc, algo) => {
|
||||
const digest = hashes.shift().digest('base64')
|
||||
const hash = new Hash(
|
||||
`${algo}-${digest}${optString}`,
|
||||
opts
|
||||
)
|
||||
/* istanbul ignore else - it would be VERY strange if the hash we
|
||||
* just calculated with an algo did not have an algo or digest.
|
||||
*/
|
||||
if (hash.algorithm && hash.digest) {
|
||||
const hashAlgo = hash.algorithm
|
||||
if (!acc[hashAlgo]) {
|
||||
acc[hashAlgo] = []
|
||||
}
|
||||
acc[hashAlgo].push(hash)
|
||||
const hash = new Hash(`${algo}-${digest}${optString}`, opts)
|
||||
if (!acc[hash.algorithm]) {
|
||||
acc[hash.algorithm] = []
|
||||
}
|
||||
acc[hash.algorithm].push(hash)
|
||||
return acc
|
||||
}, new Integrity())
|
||||
|
||||
@@ -560,18 +542,6 @@ function createIntegrity (opts) {
|
||||
}
|
||||
}
|
||||
|
||||
const NODE_HASHES = crypto.getHashes()
|
||||
|
||||
// This is a Best Effort™ at a reasonable priority for hash algos
|
||||
const DEFAULT_PRIORITY = [
|
||||
'md5', 'whirlpool', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512',
|
||||
// TODO - it's unclear _which_ of these Node will actually use as its name
|
||||
// for the algorithm, so we guesswork it based on the OpenSSL names.
|
||||
'sha3',
|
||||
'sha3-256', 'sha3-384', 'sha3-512',
|
||||
'sha3_256', 'sha3_384', 'sha3_512',
|
||||
].filter(algo => NODE_HASHES.includes(algo))
|
||||
|
||||
function getPrioritizedHash (algo1, algo2) {
|
||||
/* eslint-disable-next-line max-len */
|
||||
return DEFAULT_PRIORITY.indexOf(algo1.toLowerCase()) >= DEFAULT_PRIORITY.indexOf(algo2.toLowerCase())
|
||||
|
||||
+17
-17
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ssri",
|
||||
"version": "12.0.0",
|
||||
"version": "13.0.1",
|
||||
"description": "Standard Subresource Integrity library -- parses, serializes, generates, and verifies integrity metadata according to the SRI spec.",
|
||||
"main": "lib/index.js",
|
||||
"files": [
|
||||
@@ -11,21 +11,16 @@
|
||||
"prerelease": "npm t",
|
||||
"postrelease": "npm publish",
|
||||
"posttest": "npm run lint",
|
||||
"test": "tap",
|
||||
"test": "node --test './test/**/*.js'",
|
||||
"coverage": "tap",
|
||||
"lint": "npm run eslint",
|
||||
"postlint": "template-oss-check",
|
||||
"template-oss-apply": "template-oss-apply --force",
|
||||
"lintfix": "npm run eslint -- --fix",
|
||||
"snap": "tap",
|
||||
"eslint": "eslint \"**/*.{js,cjs,ts,mjs,jsx,tsx}\""
|
||||
},
|
||||
"tap": {
|
||||
"check-coverage": true,
|
||||
"nyc-arg": [
|
||||
"--exclude",
|
||||
"tap-snapshots/**"
|
||||
]
|
||||
"snap": "node --test --test-update-snapshots './test/**/*.js'",
|
||||
"eslint": "eslint \"**/*.{js,cjs,ts,mjs,jsx,tsx}\"",
|
||||
"test:node20": "node --test test",
|
||||
"test:cover": "node --test --experimental-test-coverage --test-timeout=3000 --test-coverage-lines=100 --test-coverage-functions=100 --test-coverage-branches=100 './test/**/*.js'"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -51,16 +46,21 @@
|
||||
"minipass": "^7.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@npmcli/eslint-config": "^5.0.0",
|
||||
"@npmcli/template-oss": "4.23.3",
|
||||
"tap": "^16.0.1"
|
||||
"@npmcli/eslint-config": "^6.0.0",
|
||||
"@npmcli/template-oss": "4.28.1",
|
||||
"benchmark": "^2.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
},
|
||||
"templateOSS": {
|
||||
"//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
|
||||
"version": "4.23.3",
|
||||
"publish": "true"
|
||||
"version": "4.28.1",
|
||||
"publish": "true",
|
||||
"allowPaths": [
|
||||
"benchmarks/"
|
||||
],
|
||||
"testRunner": "node:test",
|
||||
"latestVersion": 24
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user