Skip to content

Commit

Permalink
feat(integrityStream): new stream that can both generate and check st…
Browse files Browse the repository at this point in the history
…reamed data

BREAKING CHANGE: 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.
  • Loading branch information
zkat committed Apr 3, 2017
1 parent 02ed1ad commit fd23e1b
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 54 deletions.
28 changes: 20 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -380,14 +380,26 @@ ssri.checkStream(
) // -> Promise<Error<{code: 'EBADCHECKSUM'}>>
```

#### <a name="create-checker-stream"></a> `> createCheckerStream(sri, [opts]) -> CheckerStream`

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.

If verification fails, the returned stream will error with an `EBADCHECKSUM`
error code.
#### <a name="integrity-stream"></a> `> integrityStream(sri, [opts]) -> IntegrityStream`

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 `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 `IntegrityMetadata` 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 `IntegrityMetadata` 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
Expand Down
100 changes: 54 additions & 46 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,31 +169,16 @@ function fromData (data, opts) {
module.exports.fromStream = fromStream
function fromStream (stream, opts) {
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', () => {})
})
}

Expand All @@ -211,54 +196,77 @@ module.exports.checkStream = checkStream
function checkStream (stream, sri, opts) {
opts = opts || {}
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) => {
stream.pipe(checker)
stream.on('error', reject)
checker.on('error', reject)
checker.on('verified', meta => {
resolve(meta)
})
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 algorithm = sri.pickAlgorithm(opts)
const digests = sri[algorithm]
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) {
transform (chunk, enc, cb) {
streamSize += chunk.length
hash.update(chunk, enc)
hashes.forEach(h => h.update(chunk, enc))
cb(null, chunk, enc)
},
flush: function (cb) {
const digest = hash.digest('base64')
const match = digests.find(meta => meta.digest === digest)
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(meta => {
return newSri[algorithm].find(newmeta => {
return meta.digest === newmeta.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
return cb(err)
} else if (match) {
stream.emit('size', streamSize)
stream.emit('verified', match)
return cb()
} else {
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
err.sri = sri
return cb(err)
stream.emit('error', err)
} else {
stream.emit('size', streamSize)
stream.emit('integrity', newSri)
match && stream.emit('verified', match)
}
done()
}
})
return stream
Expand Down

0 comments on commit fd23e1b

Please sign in to comment.