Skip to content

Commit fd23e1b

Browse files
committed
feat(integrityStream): new stream that can both generate and check streamed 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.
1 parent 02ed1ad commit fd23e1b

File tree

2 files changed

+74
-54
lines changed

2 files changed

+74
-54
lines changed

Diff for: README.md

+20-8
Original file line numberDiff line numberDiff line change
@@ -380,14 +380,26 @@ ssri.checkStream(
380380
) // -> Promise<Error<{code: 'EBADCHECKSUM'}>>
381381
```
382382

383-
#### <a name="create-checker-stream"></a> `> createCheckerStream(sri, [opts]) -> CheckerStream`
384-
385-
Returns a `Through` stream that data can be piped through in order to check it
386-
against `sri`. `sri` can be any subresource integrity representation that
387-
[`ssri.parse`](#parse) can handle.
388-
389-
If verification fails, the returned stream will error with an `EBADCHECKSUM`
390-
error code.
383+
#### <a name="integrity-stream"></a> `> integrityStream(sri, [opts]) -> IntegrityStream`
384+
385+
Returns a `Transform` stream that data can be piped through in order to generate
386+
and optionally check data integrity for piped data. When the stream completes
387+
successfully, it emits `size` and `integrity` events, containing the total
388+
number of bytes processed and a calculated `Integrity` instance based on stream
389+
data, respectively.
390+
391+
If `opts.algorithms` is passed in, the listed algorithms will be calculated when
392+
generating the final `Integrity` instance. The default is `['sha512']`.
393+
394+
If `opts.single` is passed in, a single `IntegrityMetadata` instance will be
395+
returned.
396+
397+
If `opts.integrity` is passed in, it should be an `integrity` value understood
398+
by [`parse`](#parse) that the stream will check the data against. If
399+
verification succeeds, the integrity stream will emit a `verified` event whose
400+
value is a single `IntegrityMetadata` object that is the one that succeeded
401+
verification. If verification fails, the stream will error with an
402+
`EBADCHECKSUM` error code.
391403

392404
If `opts.size` is given, it will be matched against the stream size. An error
393405
with `err.code` `EBADSIZE` will be emitted by the stream if the expected size

Diff for: index.js

+54-46
Original file line numberDiff line numberDiff line change
@@ -169,31 +169,16 @@ function fromData (data, opts) {
169169
module.exports.fromStream = fromStream
170170
function fromStream (stream, opts) {
171171
opts = opts || {}
172-
const algorithms = opts.algorithms || ['sha512']
173-
const optString = opts.options && opts.options.length
174-
? `?${opts.options.join('?')}`
175-
: ''
176-
const P = opts.promise || Promise
172+
const P = opts.Promise || Promise
173+
const istream = integrityStream(opts)
177174
return new P((resolve, reject) => {
178-
const hashes = algorithms.map(algo => crypto.createHash(algo))
179-
stream.on('data', d => hashes.forEach(hash => hash.update(d)))
175+
stream.pipe(istream)
180176
stream.on('error', reject)
181-
stream.on('end', () => {
182-
resolve(algorithms.reduce((acc, algo, i) => {
183-
const hash = hashes[i]
184-
const digest = hash.digest('base64')
185-
const meta = new IntegrityMetadata(
186-
`${algo}-${digest}${optString}`,
187-
opts
188-
)
189-
if (meta.algorithm && meta.digest) {
190-
const algo = meta.algorithm
191-
if (!acc[algo]) { acc[algo] = [] }
192-
acc[algo].push(meta)
193-
}
194-
return acc
195-
}, new Integrity()))
196-
})
177+
istream.on('error', reject)
178+
let sri
179+
istream.on('integrity', s => { sri = s })
180+
istream.on('end', () => resolve(sri))
181+
istream.on('data', () => {})
197182
})
198183
}
199184

@@ -211,54 +196,77 @@ module.exports.checkStream = checkStream
211196
function checkStream (stream, sri, opts) {
212197
opts = opts || {}
213198
const P = opts.Promise || Promise
214-
const checker = createCheckerStream(sri, opts)
199+
const checker = integrityStream({
200+
integrity: sri,
201+
size: opts.size,
202+
strict: opts.strict,
203+
pickAlgorithm: opts.pickAlgorithm
204+
})
215205
return new P((resolve, reject) => {
216206
stream.pipe(checker)
217207
stream.on('error', reject)
218208
checker.on('error', reject)
219-
checker.on('verified', meta => {
220-
resolve(meta)
221-
})
209+
let sri
210+
checker.on('verified', s => { sri = s })
211+
checker.on('end', () => resolve(sri))
212+
checker.on('data', () => {})
222213
})
223214
}
224215

225-
module.exports.createCheckerStream = createCheckerStream
226-
function createCheckerStream (sri, opts) {
216+
module.exports.integrityStream = integrityStream
217+
function integrityStream (opts) {
227218
opts = opts || {}
228-
sri = parse(sri, opts)
229-
const algorithm = sri.pickAlgorithm(opts)
230-
const digests = sri[algorithm]
231-
const hash = crypto.createHash(algorithm)
219+
// For verification
220+
const sri = opts.integrity && parse(opts.integrity, opts)
221+
const algorithm = sri && sri.pickAlgorithm(opts)
222+
const digests = sri && sri[algorithm]
223+
// Calculating stream
224+
const algorithms = opts.algorithms || [algorithm || 'sha512']
225+
const hashes = algorithms.map(crypto.createHash)
232226
let streamSize = 0
233227
const stream = new Transform({
234-
transform: function (chunk, enc, cb) {
228+
transform (chunk, enc, cb) {
235229
streamSize += chunk.length
236-
hash.update(chunk, enc)
230+
hashes.forEach(h => h.update(chunk, enc))
237231
cb(null, chunk, enc)
238232
},
239-
flush: function (cb) {
240-
const digest = hash.digest('base64')
241-
const match = digests.find(meta => meta.digest === digest)
233+
flush (done) {
234+
const optString = (opts.options && opts.options.length)
235+
? `?${opts.options.join('?')}`
236+
: ''
237+
const newSri = parse(hashes.map((h, i) => {
238+
return `${algorithms[i]}-${h.digest('base64')}${optString}`
239+
}).join(' '), opts)
240+
const match = (
241+
// Integrity verification mode
242+
opts.integrity &&
243+
digests.find(meta => {
244+
return newSri[algorithm].find(newmeta => {
245+
return meta.digest === newmeta.digest
246+
})
247+
})
248+
)
242249
if (typeof opts.size === 'number' && streamSize !== opts.size) {
243250
const err = new Error(`stream size mismatch when checking ${sri}.\n Wanted: ${opts.size}\n Found: ${streamSize}`)
244251
err.code = 'EBADSIZE'
245252
err.found = streamSize
246253
err.expected = opts.size
247254
err.sri = sri
248-
return cb(err)
249-
} else if (match) {
250-
stream.emit('size', streamSize)
251-
stream.emit('verified', match)
252-
return cb()
253-
} else {
255+
stream.emit('error', err)
256+
} else if (opts.integrity && !match) {
254257
const err = new Error(`${sri} integrity checksum failed when using ${algorithm}`)
255258
err.code = 'EBADCHECKSUM'
256-
err.found = digest
259+
err.found = newSri
257260
err.expected = digests
258261
err.algorithm = algorithm
259262
err.sri = sri
260-
return cb(err)
263+
stream.emit('error', err)
264+
} else {
265+
stream.emit('size', streamSize)
266+
stream.emit('integrity', newSri)
267+
match && stream.emit('verified', match)
261268
}
269+
done()
262270
}
263271
})
264272
return stream

0 commit comments

Comments
 (0)