index.js 13 KB

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