33const crypto = require ( 'crypto' )
44const MiniPass = require ( 'minipass' )
55
6- const SPEC_ALGORITHMS = [ 'sha256' , 'sha384' , 'sha512' ]
6+ const SPEC_ALGORITHMS = [ 'sha512' , 'sha384' , 'sha256' ]
7+ const DEFAULT_ALGORITHMS = [ 'sha512' ]
78
89// TODO: this should really be a hardcoded list of algorithms we support,
910// rather than [a-z0-9].
@@ -12,80 +13,58 @@ const SRI_REGEX = /^([a-z0-9]+)-([^?]+)([?\S*]*)$/
1213const STRICT_SRI_REGEX = / ^ ( [ a - z 0 - 9 ] + ) - ( [ A - Z a - z 0 - 9 + / = ] { 44 , 88 } ) ( \? [ \x21 - \x7E ] * ) ? $ /
1314const VCHAR_REGEX = / ^ [ \x21 - \x7E ] + $ /
1415
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- const _emittedSize = Symbol ( '_emittedSize' )
34- const _emittedIntegrity = Symbol ( '_emittedIntegrity' )
35- const _emittedVerified = Symbol ( '_emittedVerified' )
16+ const getOptString = options => options ?. length ? `?${ options . join ( '?' ) } ` : ''
3617
3718class IntegrityStream extends MiniPass {
19+ #emittedIntegrity
20+ #emittedSize
21+ #emittedVerified
22+
3823 constructor ( opts ) {
3924 super ( )
4025 this . size = 0
4126 this . opts = opts
4227
4328 // may be overridden later, but set now for class consistency
44- this [ _getOptions ] ( )
29+ this . #getOptions ( )
4530
4631 // options used for calculating stream. can't be changed.
47- const { algorithms = defaultOpts . algorithms } = opts
32+ const algorithms = opts ? .algorithms || DEFAULT_ALGORITHMS
4833 this . algorithms = Array . from (
4934 new Set ( algorithms . concat ( this . algorithm ? [ this . algorithm ] : [ ] ) )
5035 )
5136 this . hashes = this . algorithms . map ( crypto . createHash )
5237 }
5338
54- [ _getOptions ] ( ) {
55- const {
56- integrity,
57- size,
58- options,
59- } = { ...defaultOpts , ...this . opts }
60-
39+ #getOptions ( ) {
6140 // For verification
62- this . sri = integrity ? parse ( integrity , this . opts ) : null
63- this . expectedSize = size
41+ this . sri = this . opts ?. integrity ? parse ( this . opts ?. integrity , this . opts ) : null
42+ this . expectedSize = this . opts ?. size
6443 this . goodSri = this . sri ? ! ! Object . keys ( this . sri ) . length : false
6544 this . algorithm = this . goodSri ? this . sri . pickAlgorithm ( this . opts ) : null
6645 this . digests = this . goodSri ? this . sri [ this . algorithm ] : null
67- this . optString = getOptString ( options )
46+ this . optString = getOptString ( this . opts ?. options )
6847 }
6948
7049 on ( ev , handler ) {
71- if ( ev === 'size' && this [ _emittedSize ] ) {
72- return handler ( this [ _emittedSize ] )
50+ if ( ev === 'size' && this . #emittedSize ) {
51+ return handler ( this . #emittedSize )
7352 }
7453
75- if ( ev === 'integrity' && this [ _emittedIntegrity ] ) {
76- return handler ( this [ _emittedIntegrity ] )
54+ if ( ev === 'integrity' && this . #emittedIntegrity ) {
55+ return handler ( this . #emittedIntegrity )
7756 }
7857
79- if ( ev === 'verified' && this [ _emittedVerified ] ) {
80- return handler ( this [ _emittedVerified ] )
58+ if ( ev === 'verified' && this . #emittedVerified ) {
59+ return handler ( this . #emittedVerified )
8160 }
8261
8362 return super . on ( ev , handler )
8463 }
8564
8665 emit ( ev , data ) {
8766 if ( ev === 'end' ) {
88- this [ _onEnd ] ( )
67+ this . #onEnd ( )
8968 }
9069 return super . emit ( ev , data )
9170 }
@@ -96,9 +75,9 @@ class IntegrityStream extends MiniPass {
9675 return super . write ( data )
9776 }
9877
99- [ _onEnd ] ( ) {
78+ #onEnd ( ) {
10079 if ( ! this . goodSri ) {
101- this [ _getOptions ] ( )
80+ this . #getOptions ( )
10281 }
10382 const newSri = parse ( this . hashes . map ( ( h , i ) => {
10483 return `${ this . algorithms [ i ] } -${ h . digest ( 'base64' ) } ${ this . optString } `
@@ -123,12 +102,12 @@ class IntegrityStream extends MiniPass {
123102 err . sri = this . sri
124103 this . emit ( 'error' , err )
125104 } else {
126- this [ _emittedSize ] = this . size
105+ this . #emittedSize = this . size
127106 this . emit ( 'size' , this . size )
128- this [ _emittedIntegrity ] = newSri
107+ this . #emittedIntegrity = newSri
129108 this . emit ( 'integrity' , newSri )
130109 if ( match ) {
131- this [ _emittedVerified ] = match
110+ this . #emittedVerified = match
132111 this . emit ( 'verified' , match )
133112 }
134113 }
@@ -141,8 +120,7 @@ class Hash {
141120 }
142121
143122 constructor ( hash , opts ) {
144- opts = ssriOpts ( opts )
145- const strict = ! ! opts . strict
123+ const strict = opts ?. strict
146124 this . source = hash . trim ( )
147125
148126 // set default values so that we make V8 happy to
@@ -161,7 +139,7 @@ class Hash {
161139 if ( ! match ) {
162140 return
163141 }
164- if ( strict && ! SPEC_ALGORITHMS . some ( a => a === match [ 1 ] ) ) {
142+ if ( strict && ! SPEC_ALGORITHMS . includes ( match [ 1 ] ) ) {
165143 return
166144 }
167145 this . algorithm = match [ 1 ]
@@ -182,14 +160,13 @@ class Hash {
182160 }
183161
184162 toString ( opts ) {
185- opts = ssriOpts ( opts )
186- if ( opts . strict ) {
163+ if ( opts ?. strict ) {
187164 // Strict mode enforces the standard as close to the foot of the
188165 // letter as it can.
189166 if ( ! (
190167 // The spec has very restricted productions for algorithms.
191168 // https://www.w3.org/TR/CSP2/#source-list-syntax
192- SPEC_ALGORITHMS . some ( x => x === this . algorithm ) &&
169+ SPEC_ALGORITHMS . includes ( this . algorithm ) &&
193170 // Usually, if someone insists on using a "different" base64, we
194171 // leave it as-is, since there's multiple standards, and the
195172 // specified is not a URL-safe variant.
@@ -203,11 +180,41 @@ class Hash {
203180 return ''
204181 }
205182 }
206- const options = this . options && this . options . length
207- ? `?${ this . options . join ( '?' ) } `
208- : ''
209- return `${ this . algorithm } -${ this . digest } ${ options } `
183+ return `${ this . algorithm } -${ this . digest } ${ getOptString ( this . options ) } `
184+ }
185+ }
186+
187+ function integrityHashToString ( toString , sep , opts , hashes ) {
188+ const toStringIsNotEmpty = toString !== ''
189+
190+ let shouldAddFirstSep = false
191+ let complement = ''
192+
193+ const lastIndex = hashes . length - 1
194+
195+ for ( let i = 0 ; i < lastIndex ; i ++ ) {
196+ const hashString = Hash . prototype . toString . call ( hashes [ i ] , opts )
197+
198+ if ( hashString ) {
199+ shouldAddFirstSep = true
200+
201+ complement += hashString
202+ complement += sep
203+ }
204+ }
205+
206+ const finalHashString = Hash . prototype . toString . call ( hashes [ lastIndex ] , opts )
207+
208+ if ( finalHashString ) {
209+ shouldAddFirstSep = true
210+ complement += finalHashString
210211 }
212+
213+ if ( toStringIsNotEmpty && shouldAddFirstSep ) {
214+ return toString + sep + complement
215+ }
216+
217+ return toString + complement
211218}
212219
213220class Integrity {
@@ -224,21 +231,28 @@ class Integrity {
224231 }
225232
226233 toString ( opts ) {
227- opts = ssriOpts ( opts )
228- let sep = opts . sep || ' '
229- if ( opts . strict ) {
234+ let sep = opts ?. sep || ' '
235+ let toString = ''
236+
237+ if ( opts ?. strict ) {
230238 // Entries must be separated by whitespace, according to spec.
231239 sep = sep . replace ( / \S + / g, ' ' )
240+
241+ for ( const hash of SPEC_ALGORITHMS ) {
242+ if ( this [ hash ] ) {
243+ toString = integrityHashToString ( toString , sep , opts , this [ hash ] )
244+ }
245+ }
246+ } else {
247+ for ( const hash of Object . keys ( this ) ) {
248+ toString = integrityHashToString ( toString , sep , opts , this [ hash ] )
249+ }
232250 }
233- return Object . keys ( this ) . map ( k => {
234- return this [ k ] . map ( hash => {
235- return Hash . prototype . toString . call ( hash , opts )
236- } ) . filter ( x => x . length ) . join ( sep )
237- } ) . filter ( x => x . length ) . join ( sep )
251+
252+ return toString
238253 }
239254
240255 concat ( integrity , opts ) {
241- opts = ssriOpts ( opts )
242256 const other = typeof integrity === 'string'
243257 ? integrity
244258 : stringify ( integrity , opts )
@@ -252,7 +266,6 @@ class Integrity {
252266 // add additional hashes to an integrity value, but prevent
253267 // *changing* an existing integrity hash.
254268 merge ( integrity , opts ) {
255- opts = ssriOpts ( opts )
256269 const other = parse ( integrity , opts )
257270 for ( const algo in other ) {
258271 if ( this [ algo ] ) {
@@ -268,7 +281,6 @@ class Integrity {
268281 }
269282
270283 match ( integrity , opts ) {
271- opts = ssriOpts ( opts )
272284 const other = parse ( integrity , opts )
273285 if ( ! other ) {
274286 return false
@@ -286,8 +298,7 @@ class Integrity {
286298 }
287299
288300 pickAlgorithm ( opts ) {
289- opts = ssriOpts ( opts )
290- const pickAlgorithm = opts . pickAlgorithm
301+ const pickAlgorithm = opts ?. pickAlgorithm || getPrioritizedHash
291302 const keys = Object . keys ( this )
292303 return keys . reduce ( ( acc , algo ) => {
293304 return pickAlgorithm ( acc , algo ) || acc
@@ -300,7 +311,6 @@ function parse (sri, opts) {
300311 if ( ! sri ) {
301312 return null
302313 }
303- opts = ssriOpts ( opts )
304314 if ( typeof sri === 'string' ) {
305315 return _parse ( sri , opts )
306316 } else if ( sri . algorithm && sri . digest ) {
@@ -315,7 +325,7 @@ function parse (sri, opts) {
315325function _parse ( integrity , opts ) {
316326 // 3.4.3. Parse metadata
317327 // https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata
318- if ( opts . single ) {
328+ if ( opts ? .single ) {
319329 return new Hash ( integrity , opts )
320330 }
321331 const hashes = integrity . trim ( ) . split ( / \s + / ) . reduce ( ( acc , string ) => {
@@ -334,7 +344,6 @@ function _parse (integrity, opts) {
334344
335345module . exports . stringify = stringify
336346function stringify ( obj , opts ) {
337- opts = ssriOpts ( opts )
338347 if ( obj . algorithm && obj . digest ) {
339348 return Hash . prototype . toString . call ( obj , opts )
340349 } else if ( typeof obj === 'string' ) {
@@ -346,8 +355,7 @@ function stringify (obj, opts) {
346355
347356module . exports . fromHex = fromHex
348357function fromHex ( hexDigest , algorithm , opts ) {
349- opts = ssriOpts ( opts )
350- const optString = getOptString ( opts . options )
358+ const optString = getOptString ( opts ?. options )
351359 return parse (
352360 `${ algorithm } -${
353361 Buffer . from ( hexDigest , 'hex' ) . toString ( 'base64' )
@@ -357,9 +365,8 @@ function fromHex (hexDigest, algorithm, opts) {
357365
358366module . exports . fromData = fromData
359367function fromData ( data , opts ) {
360- opts = ssriOpts ( opts )
361- const algorithms = opts . algorithms
362- const optString = getOptString ( opts . options )
368+ const algorithms = opts ?. algorithms || DEFAULT_ALGORITHMS
369+ const optString = getOptString ( opts ?. options )
363370 return algorithms . reduce ( ( acc , algo ) => {
364371 const digest = crypto . createHash ( algo ) . update ( data ) . digest ( 'base64' )
365372 const hash = new Hash (
@@ -382,7 +389,6 @@ function fromData (data, opts) {
382389
383390module . exports . fromStream = fromStream
384391function fromStream ( stream , opts ) {
385- opts = ssriOpts ( opts )
386392 const istream = integrityStream ( opts )
387393 return new Promise ( ( resolve , reject ) => {
388394 stream . pipe ( istream )
@@ -399,10 +405,9 @@ function fromStream (stream, opts) {
399405
400406module . exports . checkData = checkData
401407function checkData ( data , sri , opts ) {
402- opts = ssriOpts ( opts )
403408 sri = parse ( sri , opts )
404409 if ( ! sri || ! Object . keys ( sri ) . length ) {
405- if ( opts . error ) {
410+ if ( opts ? .error ) {
406411 throw Object . assign (
407412 new Error ( 'No valid integrity hashes to check against' ) , {
408413 code : 'EINTEGRITY' ,
@@ -416,7 +421,8 @@ function checkData (data, sri, opts) {
416421 const digest = crypto . createHash ( algorithm ) . update ( data ) . digest ( 'base64' )
417422 const newSri = parse ( { algorithm, digest } )
418423 const match = newSri . match ( sri , opts )
419- if ( match || ! opts . error ) {
424+ opts = opts || { }
425+ if ( match || ! ( opts . error ) ) {
420426 return match
421427 } else if ( typeof opts . size === 'number' && ( data . length !== opts . size ) ) {
422428 /* eslint-disable-next-line max-len */
@@ -440,7 +446,7 @@ function checkData (data, sri, opts) {
440446
441447module . exports . checkStream = checkStream
442448function checkStream ( stream , sri , opts ) {
443- opts = ssriOpts ( opts )
449+ opts = opts || Object . create ( null )
444450 opts . integrity = sri
445451 sri = parse ( sri , opts )
446452 if ( ! sri || ! Object . keys ( sri ) . length ) {
@@ -465,15 +471,14 @@ function checkStream (stream, sri, opts) {
465471}
466472
467473module . exports . integrityStream = integrityStream
468- function integrityStream ( opts = { } ) {
474+ function integrityStream ( opts = Object . create ( null ) ) {
469475 return new IntegrityStream ( opts )
470476}
471477
472478module . exports . create = createIntegrity
473479function createIntegrity ( opts ) {
474- opts = ssriOpts ( opts )
475- const algorithms = opts . algorithms
476- const optString = getOptString ( opts . options )
480+ const algorithms = opts ?. algorithms || DEFAULT_ALGORITHMS
481+ const optString = getOptString ( opts ?. options )
477482
478483 const hashes = algorithms . map ( crypto . createHash )
479484
0 commit comments