index.js 16 KB

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