1 | 'use strict'
|
2 |
|
3 | const crypto = require('crypto')
|
4 | const MiniPass = require('minipass')
|
5 |
|
6 | const SPEC_ALGORITHMS = ['sha256', 'sha384', 'sha512']
|
7 |
|
8 |
|
9 |
|
10 | const BASE64_REGEX = /^[a-z0-9+/]+(?:=?=?)$/i
|
11 | const SRI_REGEX = /^([a-z0-9]+)-([^?]+)([?\S*]*)$/
|
12 | const STRICT_SRI_REGEX = /^([a-z0-9]+)-([A-Za-z0-9+/=]{44,88})(\?[\x21-\x7E]*)?$/
|
13 | const VCHAR_REGEX = /^[\x21-\x7E]+$/
|
14 |
|
15 | const defaultOpts = {
|
16 | algorithms: ['sha512'],
|
17 | error: false,
|
18 | options: [],
|
19 | pickAlgorithm: getPrioritizedHash,
|
20 | sep: ' ',
|
21 | single: false,
|
22 | strict: false
|
23 | }
|
24 |
|
25 | const ssriOpts = (opts = {}) => ({ ...defaultOpts, ...opts })
|
26 |
|
27 | const getOptString = options => !options || !options.length
|
28 | ? ''
|
29 | : `?${options.join('?')}`
|
30 |
|
31 | const _onEnd = Symbol('_onEnd')
|
32 | const _getOptions = Symbol('_getOptions')
|
33 | class IntegrityStream extends MiniPass {
|
34 | constructor (opts) {
|
35 | super()
|
36 | this.size = 0
|
37 | this.opts = opts
|
38 |
|
39 |
|
40 | this[_getOptions]()
|
41 |
|
42 |
|
43 | const { algorithms = defaultOpts.algorithms } = opts
|
44 | this.algorithms = Array.from(
|
45 | new Set(algorithms.concat(this.algorithm ? [this.algorithm] : []))
|
46 | )
|
47 | this.hashes = this.algorithms.map(crypto.createHash)
|
48 | }
|
49 |
|
50 | [_getOptions] () {
|
51 | const {
|
52 | integrity,
|
53 | size,
|
54 | options
|
55 | } = { ...defaultOpts, ...this.opts }
|
56 |
|
57 |
|
58 | this.sri = integrity ? parse(integrity, this.opts) : null
|
59 | this.expectedSize = size
|
60 | this.goodSri = this.sri ? !!Object.keys(this.sri).length : false
|
61 | this.algorithm = this.goodSri ? this.sri.pickAlgorithm(this.opts) : null
|
62 | this.digests = this.goodSri ? this.sri[this.algorithm] : null
|
63 | this.optString = getOptString(options)
|
64 | }
|
65 |
|
66 | emit (ev, data) {
|
67 | if (ev === 'end') this[_onEnd]()
|
68 | return super.emit(ev, data)
|
69 | }
|
70 |
|
71 | write (data) {
|
72 | this.size += data.length
|
73 | this.hashes.forEach(h => h.update(data))
|
74 | return super.write(data)
|
75 | }
|
76 |
|
77 | [_onEnd] () {
|
78 | if (!this.goodSri) {
|
79 | this[_getOptions]()
|
80 | }
|
81 | const newSri = parse(this.hashes.map((h, i) => {
|
82 | return `${this.algorithms[i]}-${h.digest('base64')}${this.optString}`
|
83 | }).join(' '), this.opts)
|
84 |
|
85 | const match = this.goodSri && newSri.match(this.sri, this.opts)
|
86 | if (typeof this.expectedSize === 'number' && this.size !== this.expectedSize) {
|
87 | const err = new Error(`stream size mismatch when checking ${this.sri}.\n Wanted: ${this.expectedSize}\n Found: ${this.size}`)
|
88 | err.code = 'EBADSIZE'
|
89 | err.found = this.size
|
90 | err.expected = this.expectedSize
|
91 | err.sri = this.sri
|
92 | this.emit('error', err)
|
93 | } else if (this.sri && !match) {
|
94 | const err = new Error(`${this.sri} integrity checksum failed when using ${this.algorithm}: wanted ${this.digests} but got ${newSri}. (${this.size} bytes)`)
|
95 | err.code = 'EINTEGRITY'
|
96 | err.found = newSri
|
97 | err.expected = this.digests
|
98 | err.algorithm = this.algorithm
|
99 | err.sri = this.sri
|
100 | this.emit('error', err)
|
101 | } else {
|
102 | this.emit('size', this.size)
|
103 | this.emit('integrity', newSri)
|
104 | match && this.emit('verified', match)
|
105 | }
|
106 | }
|
107 | }
|
108 |
|
109 | class Hash {
|
110 | get isHash () { return true }
|
111 | constructor (hash, opts) {
|
112 | opts = ssriOpts(opts)
|
113 | const strict = !!opts.strict
|
114 | this.source = hash.trim()
|
115 |
|
116 |
|
117 |
|
118 | this.digest = ''
|
119 | this.algorithm = ''
|
120 | this.options = []
|
121 |
|
122 |
|
123 |
|
124 | const match = this.source.match(
|
125 | strict
|
126 | ? STRICT_SRI_REGEX
|
127 | : SRI_REGEX
|
128 | )
|
129 | if (!match) { return }
|
130 | if (strict && !SPEC_ALGORITHMS.some(a => a === match[1])) { return }
|
131 | this.algorithm = match[1]
|
132 | this.digest = match[2]
|
133 |
|
134 | const rawOpts = match[3]
|
135 | if (rawOpts) {
|
136 | this.options = rawOpts.slice(1).split('?')
|
137 | }
|
138 | }
|
139 |
|
140 | hexDigest () {
|
141 | return this.digest && Buffer.from(this.digest, 'base64').toString('hex')
|
142 | }
|
143 |
|
144 | toJSON () {
|
145 | return this.toString()
|
146 | }
|
147 |
|
148 | toString (opts) {
|
149 | opts = ssriOpts(opts)
|
150 | if (opts.strict) {
|
151 |
|
152 |
|
153 | if (!(
|
154 |
|
155 |
|
156 | SPEC_ALGORITHMS.some(x => x === this.algorithm) &&
|
157 |
|
158 |
|
159 |
|
160 |
|
161 | this.digest.match(BASE64_REGEX) &&
|
162 |
|
163 |
|
164 |
|
165 | this.options.every(opt => opt.match(VCHAR_REGEX))
|
166 | )) {
|
167 | return ''
|
168 | }
|
169 | }
|
170 | const options = this.options && this.options.length
|
171 | ? `?${this.options.join('?')}`
|
172 | : ''
|
173 | return `${this.algorithm}-${this.digest}${options}`
|
174 | }
|
175 | }
|
176 |
|
177 | class Integrity {
|
178 | get isIntegrity () { return true }
|
179 | toJSON () {
|
180 | return this.toString()
|
181 | }
|
182 |
|
183 | isEmpty () {
|
184 | return Object.keys(this).length === 0
|
185 | }
|
186 |
|
187 | toString (opts) {
|
188 | opts = ssriOpts(opts)
|
189 | let sep = opts.sep || ' '
|
190 | if (opts.strict) {
|
191 |
|
192 | sep = sep.replace(/\S+/g, ' ')
|
193 | }
|
194 | return Object.keys(this).map(k => {
|
195 | return this[k].map(hash => {
|
196 | return Hash.prototype.toString.call(hash, opts)
|
197 | }).filter(x => x.length).join(sep)
|
198 | }).filter(x => x.length).join(sep)
|
199 | }
|
200 |
|
201 | concat (integrity, opts) {
|
202 | opts = ssriOpts(opts)
|
203 | const other = typeof integrity === 'string'
|
204 | ? integrity
|
205 | : stringify(integrity, opts)
|
206 | return parse(`${this.toString(opts)} ${other}`, opts)
|
207 | }
|
208 |
|
209 | hexDigest () {
|
210 | return parse(this, { single: true }).hexDigest()
|
211 | }
|
212 |
|
213 |
|
214 |
|
215 | merge (integrity, opts) {
|
216 | opts = ssriOpts(opts)
|
217 | const other = parse(integrity, opts)
|
218 | for (const algo in other) {
|
219 | if (this[algo]) {
|
220 | if (!this[algo].find(hash =>
|
221 | other[algo].find(otherhash =>
|
222 | hash.digest === otherhash.digest))) {
|
223 | throw new Error('hashes do not match, cannot update integrity')
|
224 | }
|
225 | } else {
|
226 | this[algo] = other[algo]
|
227 | }
|
228 | }
|
229 | }
|
230 |
|
231 | match (integrity, opts) {
|
232 | opts = ssriOpts(opts)
|
233 | const other = parse(integrity, opts)
|
234 | const algo = other.pickAlgorithm(opts)
|
235 | return (
|
236 | this[algo] &&
|
237 | other[algo] &&
|
238 | this[algo].find(hash =>
|
239 | other[algo].find(otherhash =>
|
240 | hash.digest === otherhash.digest
|
241 | )
|
242 | )
|
243 | ) || false
|
244 | }
|
245 |
|
246 | pickAlgorithm (opts) {
|
247 | opts = ssriOpts(opts)
|
248 | const pickAlgorithm = opts.pickAlgorithm
|
249 | const keys = Object.keys(this)
|
250 | return keys.reduce((acc, algo) => {
|
251 | return pickAlgorithm(acc, algo) || acc
|
252 | })
|
253 | }
|
254 | }
|
255 |
|
256 | module.exports.parse = parse
|
257 | function parse (sri, opts) {
|
258 | if (!sri) return null
|
259 | opts = ssriOpts(opts)
|
260 | if (typeof sri === 'string') {
|
261 | return _parse(sri, opts)
|
262 | } else if (sri.algorithm && sri.digest) {
|
263 | const fullSri = new Integrity()
|
264 | fullSri[sri.algorithm] = [sri]
|
265 | return _parse(stringify(fullSri, opts), opts)
|
266 | } else {
|
267 | return _parse(stringify(sri, opts), opts)
|
268 | }
|
269 | }
|
270 |
|
271 | function _parse (integrity, opts) {
|
272 |
|
273 |
|
274 | if (opts.single) {
|
275 | return new Hash(integrity, opts)
|
276 | }
|
277 | const hashes = integrity.trim().split(/\s+/).reduce((acc, string) => {
|
278 | const hash = new Hash(string, opts)
|
279 | if (hash.algorithm && hash.digest) {
|
280 | const algo = hash.algorithm
|
281 | if (!acc[algo]) { acc[algo] = [] }
|
282 | acc[algo].push(hash)
|
283 | }
|
284 | return acc
|
285 | }, new Integrity())
|
286 | return hashes.isEmpty() ? null : hashes
|
287 | }
|
288 |
|
289 | module.exports.stringify = stringify
|
290 | function stringify (obj, opts) {
|
291 | opts = ssriOpts(opts)
|
292 | if (obj.algorithm && obj.digest) {
|
293 | return Hash.prototype.toString.call(obj, opts)
|
294 | } else if (typeof obj === 'string') {
|
295 | return stringify(parse(obj, opts), opts)
|
296 | } else {
|
297 | return Integrity.prototype.toString.call(obj, opts)
|
298 | }
|
299 | }
|
300 |
|
301 | module.exports.fromHex = fromHex
|
302 | function fromHex (hexDigest, algorithm, opts) {
|
303 | opts = ssriOpts(opts)
|
304 | const optString = getOptString(opts.options)
|
305 | return parse(
|
306 | `${algorithm}-${
|
307 | Buffer.from(hexDigest, 'hex').toString('base64')
|
308 | }${optString}`, opts
|
309 | )
|
310 | }
|
311 |
|
312 | module.exports.fromData = fromData
|
313 | function fromData (data, opts) {
|
314 | opts = ssriOpts(opts)
|
315 | const algorithms = opts.algorithms
|
316 | const optString = getOptString(opts.options)
|
317 | return algorithms.reduce((acc, algo) => {
|
318 | const digest = crypto.createHash(algo).update(data).digest('base64')
|
319 | const hash = new Hash(
|
320 | `${algo}-${digest}${optString}`,
|
321 | opts
|
322 | )
|
323 | |
324 |
|
325 |
|
326 | if (hash.algorithm && hash.digest) {
|
327 | const algo = hash.algorithm
|
328 | if (!acc[algo]) { acc[algo] = [] }
|
329 | acc[algo].push(hash)
|
330 | }
|
331 | return acc
|
332 | }, new Integrity())
|
333 | }
|
334 |
|
335 | module.exports.fromStream = fromStream
|
336 | function fromStream (stream, opts) {
|
337 | opts = ssriOpts(opts)
|
338 | const istream = integrityStream(opts)
|
339 | return new Promise((resolve, reject) => {
|
340 | stream.pipe(istream)
|
341 | stream.on('error', reject)
|
342 | istream.on('error', reject)
|
343 | let sri
|
344 | istream.on('integrity', s => { sri = s })
|
345 | istream.on('end', () => resolve(sri))
|
346 | istream.on('data', () => {})
|
347 | })
|
348 | }
|
349 |
|
350 | module.exports.checkData = checkData
|
351 | function checkData (data, sri, opts) {
|
352 | opts = ssriOpts(opts)
|
353 | sri = parse(sri, opts)
|
354 | if (!sri || !Object.keys(sri).length) {
|
355 | if (opts.error) {
|
356 | throw Object.assign(
|
357 | new Error('No valid integrity hashes to check against'), {
|
358 | code: 'EINTEGRITY'
|
359 | }
|
360 | )
|
361 | } else {
|
362 | return false
|
363 | }
|
364 | }
|
365 | const algorithm = sri.pickAlgorithm(opts)
|
366 | const digest = crypto.createHash(algorithm).update(data).digest('base64')
|
367 | const newSri = parse({ algorithm, digest })
|
368 | const match = newSri.match(sri, opts)
|
369 | if (match || !opts.error) {
|
370 | return match
|
371 | } else if (typeof opts.size === 'number' && (data.length !== opts.size)) {
|
372 | const err = new Error(`data size mismatch when checking ${sri}.\n Wanted: ${opts.size}\n Found: ${data.length}`)
|
373 | err.code = 'EBADSIZE'
|
374 | err.found = data.length
|
375 | err.expected = opts.size
|
376 | err.sri = sri
|
377 | throw err
|
378 | } else {
|
379 | const err = new Error(`Integrity checksum failed when using ${algorithm}: Wanted ${sri}, but got ${newSri}. (${data.length} bytes)`)
|
380 | err.code = 'EINTEGRITY'
|
381 | err.found = newSri
|
382 | err.expected = sri
|
383 | err.algorithm = algorithm
|
384 | err.sri = sri
|
385 | throw err
|
386 | }
|
387 | }
|
388 |
|
389 | module.exports.checkStream = checkStream
|
390 | function checkStream (stream, sri, opts) {
|
391 | opts = ssriOpts(opts)
|
392 | opts.integrity = sri
|
393 | sri = parse(sri, opts)
|
394 | if (!sri || !Object.keys(sri).length) {
|
395 | return Promise.reject(Object.assign(
|
396 | new Error('No valid integrity hashes to check against'), {
|
397 | code: 'EINTEGRITY'
|
398 | }
|
399 | ))
|
400 | }
|
401 | const checker = integrityStream(opts)
|
402 | return new Promise((resolve, reject) => {
|
403 | stream.pipe(checker)
|
404 | stream.on('error', reject)
|
405 | checker.on('error', reject)
|
406 | let sri
|
407 | checker.on('verified', s => { sri = s })
|
408 | checker.on('end', () => resolve(sri))
|
409 | checker.on('data', () => {})
|
410 | })
|
411 | }
|
412 |
|
413 | module.exports.integrityStream = integrityStream
|
414 | function integrityStream (opts = {}) {
|
415 | return new IntegrityStream(opts)
|
416 | }
|
417 |
|
418 | module.exports.create = createIntegrity
|
419 | function createIntegrity (opts) {
|
420 | opts = ssriOpts(opts)
|
421 | const algorithms = opts.algorithms
|
422 | const optString = getOptString(opts.options)
|
423 |
|
424 | const hashes = algorithms.map(crypto.createHash)
|
425 |
|
426 | return {
|
427 | update: function (chunk, enc) {
|
428 | hashes.forEach(h => h.update(chunk, enc))
|
429 | return this
|
430 | },
|
431 | digest: function (enc) {
|
432 | const integrity = algorithms.reduce((acc, algo) => {
|
433 | const digest = hashes.shift().digest('base64')
|
434 | const hash = new Hash(
|
435 | `${algo}-${digest}${optString}`,
|
436 | opts
|
437 | )
|
438 | |
439 |
|
440 |
|
441 | if (hash.algorithm && hash.digest) {
|
442 | const algo = hash.algorithm
|
443 | if (!acc[algo]) { acc[algo] = [] }
|
444 | acc[algo].push(hash)
|
445 | }
|
446 | return acc
|
447 | }, new Integrity())
|
448 |
|
449 | return integrity
|
450 | }
|
451 | }
|
452 | }
|
453 |
|
454 | const NODE_HASHES = new Set(crypto.getHashes())
|
455 |
|
456 |
|
457 | const DEFAULT_PRIORITY = [
|
458 | 'md5', 'whirlpool', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512',
|
459 |
|
460 |
|
461 | 'sha3',
|
462 | 'sha3-256', 'sha3-384', 'sha3-512',
|
463 | 'sha3_256', 'sha3_384', 'sha3_512'
|
464 | ].filter(algo => NODE_HASHES.has(algo))
|
465 |
|
466 | function getPrioritizedHash (algo1, algo2) {
|
467 | return DEFAULT_PRIORITY.indexOf(algo1.toLowerCase()) >= DEFAULT_PRIORITY.indexOf(algo2.toLowerCase())
|
468 | ? algo1
|
469 | : algo2
|
470 | }
|