Comparing version 7.0.1 to 7.1.0
@@ -5,2 +5,14 @@ # Changelog | ||
## [7.1.0](https://github.com/npm/ssri/compare/v7.0.1...v7.1.0) (2019-10-24) | ||
### Bug Fixes | ||
* Do not blow up if the opts object is mutated ([806e8c8](https://github.com/npm/ssri/commit/806e8c8)) | ||
### Features | ||
* Add Integrity#merge method ([0572c1d](https://github.com/npm/ssri/commit/0572c1d)), closes [#4](https://github.com/npm/ssri/issues/4) | ||
### [7.0.1](https://github.com/npm/ssri/compare/v7.0.0...v7.0.1) (2019-09-30) | ||
@@ -7,0 +19,0 @@ |
78
index.js
@@ -26,2 +26,7 @@ 'use strict' | ||
const getOptString = options => !options || !options.length ? '' | ||
: `?${options.join('?')}` | ||
const _onEnd = Symbol('_onEnd') | ||
const _getOptions = Symbol('_getOptions') | ||
class IntegrityStream extends MiniPass { | ||
@@ -32,8 +37,7 @@ constructor (opts) { | ||
this.opts = opts | ||
// For verification | ||
this.sri = opts.integrity && parse(opts.integrity, opts) | ||
this.goodSri = this.sri && Object.keys(this.sri).length | ||
this.algorithm = this.goodSri && this.sri.pickAlgorithm(opts) | ||
this.digests = this.goodSri && this.sri[this.algorithm] | ||
// Calculating stream | ||
// may be overridden later, but set now for class consistency | ||
this[_getOptions]() | ||
// options used for calculating stream. can't be changed. | ||
this.algorithms = Array.from( | ||
@@ -43,7 +47,17 @@ new Set(opts.algorithms.concat(this.algorithm ? [this.algorithm] : [])) | ||
this.hashes = this.algorithms.map(crypto.createHash) | ||
this.onEnd = this.onEnd.bind(this) | ||
} | ||
[_getOptions] () { | ||
const opts = this.opts | ||
// For verification | ||
this.sri = opts.integrity ? parse(opts.integrity, opts) : null | ||
this.expectedSize = opts.size | ||
this.goodSri = this.sri ? !!Object.keys(this.sri).length : false | ||
this.algorithm = this.goodSri ? this.sri.pickAlgorithm(opts) : null | ||
this.digests = this.goodSri ? this.sri[this.algorithm] : null | ||
this.optString = getOptString(opts.options) | ||
} | ||
emit (ev, data) { | ||
if (ev === 'end') this.onEnd() | ||
if (ev === 'end') this[_onEnd]() | ||
return super.emit(ev, data) | ||
@@ -58,19 +72,19 @@ } | ||
onEnd () { | ||
const optString = (this.opts.options && this.opts.options.length) | ||
? `?${this.opts.options.join('?')}` | ||
: '' | ||
[_onEnd] () { | ||
if (!this.goodSri) { | ||
this[_getOptions]() | ||
} | ||
const newSri = parse(this.hashes.map((h, i) => { | ||
return `${this.algorithms[i]}-${h.digest('base64')}${optString}` | ||
return `${this.algorithms[i]}-${h.digest('base64')}${this.optString}` | ||
}).join(' '), this.opts) | ||
// Integrity verification mode | ||
const match = this.goodSri && newSri.match(this.sri, this.opts) | ||
if (typeof this.opts.size === 'number' && this.size !== this.opts.size) { | ||
const err = new Error(`stream size mismatch when checking ${this.sri}.\n Wanted: ${this.opts.size}\n Found: ${this.size}`) | ||
if (typeof this.expectedSize === 'number' && this.size !== this.expectedSize) { | ||
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 | ||
err.expected = this.opts.size | ||
err.expected = this.expectedSize | ||
err.sri = this.sri | ||
this.emit('error', err) | ||
} else if (this.opts.integrity && !match) { | ||
} else if (this.sri && !match) { | ||
const err = new Error(`${this.sri} integrity checksum failed when using ${this.algorithm}: wanted ${this.digests} but got ${newSri}. (${this.size} bytes)`) | ||
@@ -191,2 +205,20 @@ err.code = 'EINTEGRITY' | ||
// 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]) { | ||
if (!this[algo].find(hash => | ||
other[algo].find(otherhash => | ||
hash.digest === otherhash.digest))) { | ||
throw new Error('hashes do not match, cannot update integrity') | ||
} | ||
} else { | ||
this[algo] = other[algo] | ||
} | ||
} | ||
} | ||
match (integrity, opts) { | ||
@@ -268,5 +300,3 @@ opts = SsriOpts(opts) | ||
opts = SsriOpts(opts) | ||
const optString = opts.options && opts.options.length | ||
? `?${opts.options.join('?')}` | ||
: '' | ||
const optString = getOptString(opts.options) | ||
return parse( | ||
@@ -283,5 +313,3 @@ `${algorithm}-${ | ||
const algorithms = opts.algorithms | ||
const optString = opts.options && opts.options.length | ||
? `?${opts.options.join('?')}` | ||
: '' | ||
const optString = getOptString(opts.options) | ||
return algorithms.reduce((acc, algo) => { | ||
@@ -385,5 +413,3 @@ const digest = crypto.createHash(algo).update(data).digest('base64') | ||
const algorithms = opts.algorithms | ||
const optString = opts.options.length | ||
? `?${opts.options.join('?')}` | ||
: '' | ||
const optString = getOptString(opts.options) | ||
@@ -390,0 +416,0 @@ const hashes = algorithms.map(crypto.createHash) |
{ | ||
"name": "ssri", | ||
"version": "7.0.1", | ||
"version": "7.1.0", | ||
"description": "Standard Subresource Integrity library -- parses, serializes, generates, and verifies integrity metadata according to the SRI spec.", | ||
@@ -41,3 +41,3 @@ "main": "index.js", | ||
"figgy-pudding": "^3.5.1", | ||
"minipass": "^3.0.0" | ||
"minipass": "^3.1.1" | ||
}, | ||
@@ -47,3 +47,3 @@ "devDependencies": { | ||
"standard-version": "^7.0.0", | ||
"tap": "^14.6.9", | ||
"tap": "^14.8.2", | ||
"weallbehave": "^1.2.0", | ||
@@ -50,0 +50,0 @@ "weallcontribute": "^1.0.8" |
@@ -22,2 +22,3 @@ # 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/npm/ssri.svg)](https://travis-ci.org/npm/ssri) [![AppVeyor](https://ci.appveyor.com/api/projects/status/github/npm/ssri?svg=true)](https://ci.appveyor.com/project/npm/ssri) [![Coverage Status](https://coveralls.io/repos/github/npm/ssri/badge.svg?branch=latest)](https://coveralls.io/github/npm/ssri?branch=latest) | ||
* [`Integrity#concat`](#integrity-concat) | ||
* [`Integrity#merge`](#integrity-merge) | ||
* [`Integrity#toString`](#integrity-to-string) | ||
@@ -188,2 +189,41 @@ * [`Integrity#toJSON`](#integrity-to-json) | ||
#### <a name="integrity-merge"></a> `> Integrity#merge(otherIntegrity, [opts])` | ||
Safely merges another IntegrityLike or integrity string into an `Integrity` | ||
object. | ||
If the other integrity value has any algorithms in common with the current | ||
object, then the hash digests must match, or an error is thrown. | ||
Any new hashes will be added to the current object's set. | ||
This is useful when an integrity value may be upgraded with a stronger | ||
algorithm, you wish to prevent accidentally supressing integrity errors by | ||
overwriting the expected integrity value. | ||
##### Example | ||
```javascript | ||
const data = fs.readFileSync('data.txt') | ||
// integrity.txt contains 'sha1-X1UT+IIv2+UUWvM7ZNjZcNz5XG4=' | ||
// because we were young, and didn't realize sha1 would not last | ||
const expectedIntegrity = ssri.parse(fs.readFileSync('integrity.txt', 'utf8')) | ||
const match = ssri.checkData(data, expectedIntegrity, { | ||
algorithms: ['sha512', 'sha1'] | ||
}) | ||
if (!match) { | ||
throw new Error('data corrupted or something!') | ||
} | ||
// get a stronger algo! | ||
if (match && match.algorithm !== 'sha512') { | ||
const updatedIntegrity = ssri.fromData(data, { algorithms: ['sha512'] }) | ||
expectedIntegrity.merge(updatedIntegrity) | ||
fs.writeFileSync('integrity.txt', expectedIntegrity.toString()) | ||
// file now contains | ||
// 'sha1-X1UT+IIv2+UUWvM7ZNjZcNz5XG4= sha512-yzd8ELD1piyANiWnmdnpCL5F52f10UfUdEkHywVZeqTt0ymgrxR63Qz0GB7TKPoeeZQmWCaz7T1+9vBnypkYWg==' | ||
} | ||
``` | ||
#### <a name="integrity-to-string"></a> `> Integrity#toString([opts]) -> String` | ||
@@ -190,0 +230,0 @@ |
45632
412
529
Updatedminipass@^3.1.1