From 0572c1d7ccfd0823f97538dad208066717da30c3 Mon Sep 17 00:00:00 2001 From: isaacs Date: Thu, 24 Oct 2019 15:41:43 -0700 Subject: [PATCH] feat: Add Integrity#merge method This is going to be used in Pacote to upgrade the Fetcher.integrity value with the data that comes out of cacache, but in such a way that we don't accidentally suppress integrity errors. PR-URL: https://github.com/npm/ssri/pull/4 Credit: @isaacs Close: #4 Reviewed-by: @isaacs --- README.md | 40 ++++++++++++++++++++++++++++++++++++++++ index.js | 22 ++++++++++++++++++++-- test/update.js | 13 +++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 test/update.js diff --git a/README.md b/README.md index e0e2bc1..0cd41be 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Integrity](https://w3c.github.io/webappsec/specs/subresourceintegrity/) hashes. * [`parse`](#parse) * [`stringify`](#stringify) * [`Integrity#concat`](#integrity-concat) + * [`Integrity#merge`](#integrity-merge) * [`Integrity#toString`](#integrity-to-string) * [`Integrity#toJSON`](#integrity-to-json) * [`Integrity#match`](#integrity-match) @@ -184,6 +185,45 @@ const mobileIntegrity = ssri.fromData(fs.readFileSync('./index.mobile.js')) desktopIntegrity.concat(mobileIntegrity) ``` +#### `> 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==' +} +``` + #### `> Integrity#toString([opts]) -> String` Returns the string representation of an `Integrity` object. All hash entries diff --git a/index.js b/index.js index dce61af..daee85d 100644 --- a/index.js +++ b/index.js @@ -47,9 +47,9 @@ class IntegrityStream extends MiniPass { [_getOptions] () { const opts = this.opts // For verification - this.sri = opts.integrity && parse(opts.integrity, opts) + this.sri = opts.integrity ? parse(opts.integrity, opts) : null this.expectedSize = opts.size - this.goodSri = this.sri ? Object.keys(this.sri).length || null : null + 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) @@ -198,6 +198,24 @@ class Integrity { return parse(this, { single: true }).hexDigest() } + // 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) { opts = SsriOpts(opts) const other = parse(integrity, opts) diff --git a/test/update.js b/test/update.js new file mode 100644 index 0000000..aa16de5 --- /dev/null +++ b/test/update.js @@ -0,0 +1,13 @@ +const ssri = require('../') +const t = require('tap') + +const i = ssri.parse('sha1-foo') +const o = ssri.parse('sha512-bar') +i.merge(o) +t.equal(i.toString(), 'sha1-foo sha512-bar', 'added second algo') +t.throws(() => i.merge(ssri.parse('sha1-baz')), { + message: 'hashes do not match, cannot update integrity' +}) +i.merge(o) +i.merge(ssri.parse('sha1-foo')) +t.equal(i.toString(), 'sha1-foo sha512-bar', 'did not duplicate')