1'use strict' 2 3const crypto = require('crypto') 4const figgyPudding = require('figgy-pudding') 5const Transform = require('stream').Transform 6 7const SPEC_ALGORITHMS = ['sha256', 'sha384', 'sha512'] 8 9const BASE64_REGEX = /^[a-z0-9+/]+(?:=?=?)$/i 10const SRI_REGEX = /^([^-]+)-([^?]+)([?\S*]*)$/ 11const STRICT_SRI_REGEX = /^([^-]+)-([A-Za-z0-9+/=]{44,88})(\?[\x21-\x7E]*)?$/ 12const VCHAR_REGEX = /^[\x21-\x7E]+$/ 13 14const SsriOpts = figgyPudding({ 15 algorithms: {default: ['sha512']}, 16 error: {default: false}, 17 integrity: {}, 18 options: {default: []}, 19 pickAlgorithm: {default: () => getPrioritizedHash}, 20 Promise: {default: () => Promise}, 21 sep: {default: ' '}, 22 single: {default: false}, 23 size: {}, 24 strict: {default: false} 25}) 26 27class Hash { 28 get isHash () { return true } 29 constructor (hash, opts) { 30 opts = SsriOpts(opts) 31 const strict = !!opts.strict 32 this.source = hash.trim() 33 // 3.1. Integrity metadata (called "Hash" by ssri) 34 // https://w3c.github.io/webappsec-subresource-integrity/#integrity-metadata-description 35 const match = this.source.match( 36 strict 37 ? STRICT_SRI_REGEX 38 : SRI_REGEX 39 ) 40 if (!match) { return } 41 if (strict && !SPEC_ALGORITHMS.some(a => a === match[1])) { return } 42 this.algorithm = match[1] 43 this.digest = match[2] 44 45 const rawOpts = match[3] 46 this.options = rawOpts ? rawOpts.slice(1).split('?') : [] 47 } 48 hexDigest () { 49 return this.digest && Buffer.from(this.digest, 'base64').toString('hex') 50 } 51 toJSON () { 52 return this.toString() 53 } 54 toString (opts) { 55 opts = SsriOpts(opts) 56 if (opts.strict) { 57 // Strict mode enforces the standard as close to the foot of the 58 // letter as it can. 59 if (!( 60 // The spec has very restricted productions for algorithms. 61 // https://www.w3.org/TR/CSP2/#source-list-syntax 62 SPEC_ALGORITHMS.some(x => x === this.algorithm) && 63 // Usually, if someone insists on using a "different" base64, we 64 // leave it as-is, since there's multiple standards, and the 65 // specified is not a URL-safe variant. 66 // https://www.w3.org/TR/CSP2/#base64_value 67 this.digest.match(BASE64_REGEX) && 68 // Option syntax is strictly visual chars. 69 // https://w3c.github.io/webappsec-subresource-integrity/#grammardef-option-expression 70 // https://tools.ietf.org/html/rfc5234#appendix-B.1 71 (this.options || []).every(opt => opt.match(VCHAR_REGEX)) 72 )) { 73 return '' 74 } 75 } 76 const options = this.options && this.options.length 77 ? `?${this.options.join('?')}` 78 : '' 79 return `${this.algorithm}-${this.digest}${options}` 80 } 81} 82 83class Integrity { 84 get isIntegrity () { return true } 85 toJSON () { 86 return this.toString() 87 } 88 toString (opts) { 89 opts = SsriOpts(opts) 90 let sep = opts.sep || ' ' 91 if (opts.strict) { 92 // Entries must be separated by whitespace, according to spec. 93 sep = sep.replace(/\S+/g, ' ') 94 } 95 return Object.keys(this).map(k => { 96 return this[k].map(hash => { 97 return Hash.prototype.toString.call(hash, opts) 98 }).filter(x => x.length).join(sep) 99 }).filter(x => x.length).join(sep) 100 } 101 concat (integrity, opts) { 102 opts = SsriOpts(opts) 103 const other = typeof integrity === 'string' 104 ? integrity 105 : stringify(integrity, opts) 106 return parse(`${this.toString(opts)} ${other}`, opts) 107 } 108 hexDigest () { 109 return parse(this, {single: true}).hexDigest() 110 } 111 match (integrity, opts) { 112 opts = SsriOpts(opts) 113 const other = parse(integrity, opts) 114 const algo = other.pickAlgorithm(opts) 115 return ( 116 this[algo] && 117 other[algo] && 118 this[algo].find(hash => 119 other[algo].find(otherhash => 120 hash.digest === otherhash.digest 121 ) 122 ) 123 ) || false 124 } 125 pickAlgorithm (opts) { 126 opts = SsriOpts(opts) 127 const pickAlgorithm = opts.pickAlgorithm 128 const keys = Object.keys(this) 129 if (!keys.length) { 130 throw new Error(`No algorithms available for ${ 131 JSON.stringify(this.toString()) 132 }`) 133 } 134 return keys.reduce((acc, algo) => { 135 return pickAlgorithm(acc, algo) || acc 136 }) 137 } 138} 139 140module.exports.parse = parse 141function parse (sri, opts) { 142 opts = SsriOpts(opts) 143 if (typeof sri === 'string') { 144 return _parse(sri, opts) 145 } else if (sri.algorithm && sri.digest) { 146 const fullSri = new Integrity() 147 fullSri[sri.algorithm] = [sri] 148 return _parse(stringify(fullSri, opts), opts) 149 } else { 150 return _parse(stringify(sri, opts), opts) 151 } 152} 153 154function _parse (integrity, opts) { 155 // 3.4.3. Parse metadata 156 // https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata 157 if (opts.single) { 158 return new Hash(integrity, opts) 159 } 160 return integrity.trim().split(/\s+/).reduce((acc, string) => { 161 const hash = new Hash(string, opts) 162 if (hash.algorithm && hash.digest) { 163 const algo = hash.algorithm 164 if (!acc[algo]) { acc[algo] = [] } 165 acc[algo].push(hash) 166 } 167 return acc 168 }, new Integrity()) 169} 170 171module.exports.stringify = stringify 172function stringify (obj, opts) { 173 opts = SsriOpts(opts) 174 if (obj.algorithm && obj.digest) { 175 return Hash.prototype.toString.call(obj, opts) 176 } else if (typeof obj === 'string') { 177 return stringify(parse(obj, opts), opts) 178 } else { 179 return Integrity.prototype.toString.call(obj, opts) 180 } 181} 182 183module.exports.fromHex = fromHex 184function fromHex (hexDigest, algorithm, opts) { 185 opts = SsriOpts(opts) 186 const optString = opts.options && opts.options.length 187 ? `?${opts.options.join('?')}` 188 : '' 189 return parse( 190 `${algorithm}-${ 191 Buffer.from(hexDigest, 'hex').toString('base64') 192 }${optString}`, opts 193 ) 194} 195 196module.exports.fromData = fromData 197function fromData (data, opts) { 198 opts = SsriOpts(opts) 199 const algorithms = opts.algorithms 200 const optString = opts.options && opts.options.length 201 ? `?${opts.options.join('?')}` 202 : '' 203 return algorithms.reduce((acc, algo) => { 204 const digest = crypto.createHash(algo).update(data).digest('base64') 205 const hash = new Hash( 206 `${algo}-${digest}${optString}`, 207 opts 208 ) 209 if (hash.algorithm && hash.digest) { 210 const algo = hash.algorithm 211 if (!acc[algo]) { acc[algo] = [] } 212 acc[algo].push(hash) 213 } 214 return acc 215 }, new Integrity()) 216} 217 218module.exports.fromStream = fromStream 219function fromStream (stream, opts) { 220 opts = SsriOpts(opts) 221 const P = opts.Promise || Promise 222 const istream = integrityStream(opts) 223 return new P((resolve, reject) => { 224 stream.pipe(istream) 225 stream.on('error', reject) 226 istream.on('error', reject) 227 let sri 228 istream.on('integrity', s => { sri = s }) 229 istream.on('end', () => resolve(sri)) 230 istream.on('data', () => {}) 231 }) 232} 233 234module.exports.checkData = checkData 235function checkData (data, sri, opts) { 236 opts = SsriOpts(opts) 237 sri = parse(sri, opts) 238 if (!Object.keys(sri).length) { 239 if (opts.error) { 240 throw Object.assign( 241 new Error('No valid integrity hashes to check against'), { 242 code: 'EINTEGRITY' 243 } 244 ) 245 } else { 246 return false 247 } 248 } 249 const algorithm = sri.pickAlgorithm(opts) 250 const digest = crypto.createHash(algorithm).update(data).digest('base64') 251 const newSri = parse({algorithm, digest}) 252 const match = newSri.match(sri, opts) 253 if (match || !opts.error) { 254 return match 255 } else if (typeof opts.size === 'number' && (data.length !== opts.size)) { 256 const err = new Error(`data size mismatch when checking ${sri}.\n Wanted: ${opts.size}\n Found: ${data.length}`) 257 err.code = 'EBADSIZE' 258 err.found = data.length 259 err.expected = opts.size 260 err.sri = sri 261 throw err 262 } else { 263 const err = new Error(`Integrity checksum failed when using ${algorithm}: Wanted ${sri}, but got ${newSri}. (${data.length} bytes)`) 264 err.code = 'EINTEGRITY' 265 err.found = newSri 266 err.expected = sri 267 err.algorithm = algorithm 268 err.sri = sri 269 throw err 270 } 271} 272 273module.exports.checkStream = checkStream 274function checkStream (stream, sri, opts) { 275 opts = SsriOpts(opts) 276 const P = opts.Promise || Promise 277 const checker = integrityStream(opts.concat({ 278 integrity: sri 279 })) 280 return new P((resolve, reject) => { 281 stream.pipe(checker) 282 stream.on('error', reject) 283 checker.on('error', reject) 284 let sri 285 checker.on('verified', s => { sri = s }) 286 checker.on('end', () => resolve(sri)) 287 checker.on('data', () => {}) 288 }) 289} 290 291module.exports.integrityStream = integrityStream 292function integrityStream (opts) { 293 opts = SsriOpts(opts) 294 // For verification 295 const sri = opts.integrity && parse(opts.integrity, opts) 296 const goodSri = sri && Object.keys(sri).length 297 const algorithm = goodSri && sri.pickAlgorithm(opts) 298 const digests = goodSri && sri[algorithm] 299 // Calculating stream 300 const algorithms = Array.from( 301 new Set(opts.algorithms.concat(algorithm ? [algorithm] : [])) 302 ) 303 const hashes = algorithms.map(crypto.createHash) 304 let streamSize = 0 305 const stream = new Transform({ 306 transform (chunk, enc, cb) { 307 streamSize += chunk.length 308 hashes.forEach(h => h.update(chunk, enc)) 309 cb(null, chunk, enc) 310 } 311 }).on('end', () => { 312 const optString = (opts.options && opts.options.length) 313 ? `?${opts.options.join('?')}` 314 : '' 315 const newSri = parse(hashes.map((h, i) => { 316 return `${algorithms[i]}-${h.digest('base64')}${optString}` 317 }).join(' '), opts) 318 // Integrity verification mode 319 const match = goodSri && newSri.match(sri, opts) 320 if (typeof opts.size === 'number' && streamSize !== opts.size) { 321 const err = new Error(`stream size mismatch when checking ${sri}.\n Wanted: ${opts.size}\n Found: ${streamSize}`) 322 err.code = 'EBADSIZE' 323 err.found = streamSize 324 err.expected = opts.size 325 err.sri = sri 326 stream.emit('error', err) 327 } else if (opts.integrity && !match) { 328 const err = new Error(`${sri} integrity checksum failed when using ${algorithm}: wanted ${digests} but got ${newSri}. (${streamSize} bytes)`) 329 err.code = 'EINTEGRITY' 330 err.found = newSri 331 err.expected = digests 332 err.algorithm = algorithm 333 err.sri = sri 334 stream.emit('error', err) 335 } else { 336 stream.emit('size', streamSize) 337 stream.emit('integrity', newSri) 338 match && stream.emit('verified', match) 339 } 340 }) 341 return stream 342} 343 344module.exports.create = createIntegrity 345function createIntegrity (opts) { 346 opts = SsriOpts(opts) 347 const algorithms = opts.algorithms 348 const optString = opts.options.length 349 ? `?${opts.options.join('?')}` 350 : '' 351 352 const hashes = algorithms.map(crypto.createHash) 353 354 return { 355 update: function (chunk, enc) { 356 hashes.forEach(h => h.update(chunk, enc)) 357 return this 358 }, 359 digest: function (enc) { 360 const integrity = algorithms.reduce((acc, algo) => { 361 const digest = hashes.shift().digest('base64') 362 const hash = new Hash( 363 `${algo}-${digest}${optString}`, 364 opts 365 ) 366 if (hash.algorithm && hash.digest) { 367 const algo = hash.algorithm 368 if (!acc[algo]) { acc[algo] = [] } 369 acc[algo].push(hash) 370 } 371 return acc 372 }, new Integrity()) 373 374 return integrity 375 } 376 } 377} 378 379const NODE_HASHES = new Set(crypto.getHashes()) 380 381// This is a Best Effort™ at a reasonable priority for hash algos 382const DEFAULT_PRIORITY = [ 383 'md5', 'whirlpool', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512', 384 // TODO - it's unclear _which_ of these Node will actually use as its name 385 // for the algorithm, so we guesswork it based on the OpenSSL names. 386 'sha3', 387 'sha3-256', 'sha3-384', 'sha3-512', 388 'sha3_256', 'sha3_384', 'sha3_512' 389].filter(algo => NODE_HASHES.has(algo)) 390 391function getPrioritizedHash (algo1, algo2) { 392 return DEFAULT_PRIORITY.indexOf(algo1.toLowerCase()) >= DEFAULT_PRIORITY.indexOf(algo2.toLowerCase()) 393 ? algo1 394 : algo2 395} 396