Skip to content

Commit

Permalink
feat: Add Integrity#merge method
Browse files Browse the repository at this point in the history
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: #4
Credit: @isaacs
Close: #4
Reviewed-by: @isaacs
  • Loading branch information
isaacs committed Oct 24, 2019
1 parent 3084efd commit 0572c1d
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 2 deletions.
40 changes: 40 additions & 0 deletions README.md
Expand Up @@ -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)
Expand Down Expand Up @@ -184,6 +185,45 @@ const mobileIntegrity = ssri.fromData(fs.readFileSync('./index.mobile.js'))
desktopIntegrity.concat(mobileIntegrity)
```

#### <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`

Returns the string representation of an `Integrity` object. All hash entries
Expand Down
22 changes: 20 additions & 2 deletions index.js
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions 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')

0 comments on commit 0572c1d

Please sign in to comment.