index.js 11 KB

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