diff --git a/README.md b/README.md index 3b6fba0..499b090 100644 --- a/README.md +++ b/README.md @@ -380,14 +380,26 @@ ssri.checkStream( ) // -> Promise> ``` -#### `> 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. +#### `> 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 diff --git a/index.js b/index.js index 8141612..94d3d32 100644 --- a/index.js +++ b/index.js @@ -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', () => {}) }) } @@ -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