3
3
const crypto = require ( 'crypto' )
4
4
const MiniPass = require ( 'minipass' )
5
5
6
- const SPEC_ALGORITHMS = [ 'sha256' , 'sha384' , 'sha512' ]
6
+ const SPEC_ALGORITHMS = [ 'sha512' , 'sha384' , 'sha256' ]
7
+ const DEFAULT_ALGORITHMS = [ 'sha512' ]
7
8
8
9
// TODO: this should really be a hardcoded list of algorithms we support,
9
10
// rather than [a-z0-9].
@@ -12,80 +13,58 @@ const SRI_REGEX = /^([a-z0-9]+)-([^?]+)([?\S*]*)$/
12
13
const STRICT_SRI_REGEX = / ^ ( [ a - z 0 - 9 ] + ) - ( [ A - Z a - z 0 - 9 + / = ] { 44 , 88 } ) ( \? [ \x21 - \x7E ] * ) ? $ /
13
14
const VCHAR_REGEX = / ^ [ \x21 - \x7E ] + $ /
14
15
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 ( '?' ) } ` : ''
36
17
37
18
class IntegrityStream extends MiniPass {
19
+ #emittedIntegrity
20
+ #emittedSize
21
+ #emittedVerified
22
+
38
23
constructor ( opts ) {
39
24
super ( )
40
25
this . size = 0
41
26
this . opts = opts
42
27
43
28
// may be overridden later, but set now for class consistency
44
- this [ _getOptions ] ( )
29
+ this . #getOptions ( )
45
30
46
31
// options used for calculating stream. can't be changed.
47
- const { algorithms = defaultOpts . algorithms } = opts
32
+ const algorithms = opts ? .algorithms || DEFAULT_ALGORITHMS
48
33
this . algorithms = Array . from (
49
34
new Set ( algorithms . concat ( this . algorithm ? [ this . algorithm ] : [ ] ) )
50
35
)
51
36
this . hashes = this . algorithms . map ( crypto . createHash )
52
37
}
53
38
54
- [ _getOptions ] ( ) {
55
- const {
56
- integrity,
57
- size,
58
- options,
59
- } = { ...defaultOpts , ...this . opts }
60
-
39
+ #getOptions ( ) {
61
40
// 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
64
43
this . goodSri = this . sri ? ! ! Object . keys ( this . sri ) . length : false
65
44
this . algorithm = this . goodSri ? this . sri . pickAlgorithm ( this . opts ) : null
66
45
this . digests = this . goodSri ? this . sri [ this . algorithm ] : null
67
- this . optString = getOptString ( options )
46
+ this . optString = getOptString ( this . opts ?. options )
68
47
}
69
48
70
49
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 )
73
52
}
74
53
75
- if ( ev === 'integrity' && this [ _emittedIntegrity ] ) {
76
- return handler ( this [ _emittedIntegrity ] )
54
+ if ( ev === 'integrity' && this . #emittedIntegrity ) {
55
+ return handler ( this . #emittedIntegrity )
77
56
}
78
57
79
- if ( ev === 'verified' && this [ _emittedVerified ] ) {
80
- return handler ( this [ _emittedVerified ] )
58
+ if ( ev === 'verified' && this . #emittedVerified ) {
59
+ return handler ( this . #emittedVerified )
81
60
}
82
61
83
62
return super . on ( ev , handler )
84
63
}
85
64
86
65
emit ( ev , data ) {
87
66
if ( ev === 'end' ) {
88
- this [ _onEnd ] ( )
67
+ this . #onEnd ( )
89
68
}
90
69
return super . emit ( ev , data )
91
70
}
@@ -96,9 +75,9 @@ class IntegrityStream extends MiniPass {
96
75
return super . write ( data )
97
76
}
98
77
99
- [ _onEnd ] ( ) {
78
+ #onEnd ( ) {
100
79
if ( ! this . goodSri ) {
101
- this [ _getOptions ] ( )
80
+ this . #getOptions ( )
102
81
}
103
82
const newSri = parse ( this . hashes . map ( ( h , i ) => {
104
83
return `${ this . algorithms [ i ] } -${ h . digest ( 'base64' ) } ${ this . optString } `
@@ -123,12 +102,12 @@ class IntegrityStream extends MiniPass {
123
102
err . sri = this . sri
124
103
this . emit ( 'error' , err )
125
104
} else {
126
- this [ _emittedSize ] = this . size
105
+ this . #emittedSize = this . size
127
106
this . emit ( 'size' , this . size )
128
- this [ _emittedIntegrity ] = newSri
107
+ this . #emittedIntegrity = newSri
129
108
this . emit ( 'integrity' , newSri )
130
109
if ( match ) {
131
- this [ _emittedVerified ] = match
110
+ this . #emittedVerified = match
132
111
this . emit ( 'verified' , match )
133
112
}
134
113
}
@@ -141,8 +120,7 @@ class Hash {
141
120
}
142
121
143
122
constructor ( hash , opts ) {
144
- opts = ssriOpts ( opts )
145
- const strict = ! ! opts . strict
123
+ const strict = opts ?. strict
146
124
this . source = hash . trim ( )
147
125
148
126
// set default values so that we make V8 happy to
@@ -161,7 +139,7 @@ class Hash {
161
139
if ( ! match ) {
162
140
return
163
141
}
164
- if ( strict && ! SPEC_ALGORITHMS . some ( a => a === match [ 1 ] ) ) {
142
+ if ( strict && ! SPEC_ALGORITHMS . includes ( match [ 1 ] ) ) {
165
143
return
166
144
}
167
145
this . algorithm = match [ 1 ]
@@ -182,14 +160,13 @@ class Hash {
182
160
}
183
161
184
162
toString ( opts ) {
185
- opts = ssriOpts ( opts )
186
- if ( opts . strict ) {
163
+ if ( opts ?. strict ) {
187
164
// Strict mode enforces the standard as close to the foot of the
188
165
// letter as it can.
189
166
if ( ! (
190
167
// The spec has very restricted productions for algorithms.
191
168
// https://www.w3.org/TR/CSP2/#source-list-syntax
192
- SPEC_ALGORITHMS . some ( x => x === this . algorithm ) &&
169
+ SPEC_ALGORITHMS . includes ( this . algorithm ) &&
193
170
// Usually, if someone insists on using a "different" base64, we
194
171
// leave it as-is, since there's multiple standards, and the
195
172
// specified is not a URL-safe variant.
@@ -203,11 +180,41 @@ class Hash {
203
180
return ''
204
181
}
205
182
}
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
210
211
}
212
+
213
+ if ( toStringIsNotEmpty && shouldAddFirstSep ) {
214
+ return toString + sep + complement
215
+ }
216
+
217
+ return toString + complement
211
218
}
212
219
213
220
class Integrity {
@@ -224,21 +231,28 @@ class Integrity {
224
231
}
225
232
226
233
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 ) {
230
238
// Entries must be separated by whitespace, according to spec.
231
239
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
+ }
232
250
}
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
238
253
}
239
254
240
255
concat ( integrity , opts ) {
241
- opts = ssriOpts ( opts )
242
256
const other = typeof integrity === 'string'
243
257
? integrity
244
258
: stringify ( integrity , opts )
@@ -252,7 +266,6 @@ class Integrity {
252
266
// add additional hashes to an integrity value, but prevent
253
267
// *changing* an existing integrity hash.
254
268
merge ( integrity , opts ) {
255
- opts = ssriOpts ( opts )
256
269
const other = parse ( integrity , opts )
257
270
for ( const algo in other ) {
258
271
if ( this [ algo ] ) {
@@ -268,7 +281,6 @@ class Integrity {
268
281
}
269
282
270
283
match ( integrity , opts ) {
271
- opts = ssriOpts ( opts )
272
284
const other = parse ( integrity , opts )
273
285
if ( ! other ) {
274
286
return false
@@ -286,8 +298,7 @@ class Integrity {
286
298
}
287
299
288
300
pickAlgorithm ( opts ) {
289
- opts = ssriOpts ( opts )
290
- const pickAlgorithm = opts . pickAlgorithm
301
+ const pickAlgorithm = opts ?. pickAlgorithm || getPrioritizedHash
291
302
const keys = Object . keys ( this )
292
303
return keys . reduce ( ( acc , algo ) => {
293
304
return pickAlgorithm ( acc , algo ) || acc
@@ -300,7 +311,6 @@ function parse (sri, opts) {
300
311
if ( ! sri ) {
301
312
return null
302
313
}
303
- opts = ssriOpts ( opts )
304
314
if ( typeof sri === 'string' ) {
305
315
return _parse ( sri , opts )
306
316
} else if ( sri . algorithm && sri . digest ) {
@@ -315,7 +325,7 @@ function parse (sri, opts) {
315
325
function _parse ( integrity , opts ) {
316
326
// 3.4.3. Parse metadata
317
327
// https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata
318
- if ( opts . single ) {
328
+ if ( opts ? .single ) {
319
329
return new Hash ( integrity , opts )
320
330
}
321
331
const hashes = integrity . trim ( ) . split ( / \s + / ) . reduce ( ( acc , string ) => {
@@ -334,7 +344,6 @@ function _parse (integrity, opts) {
334
344
335
345
module . exports . stringify = stringify
336
346
function stringify ( obj , opts ) {
337
- opts = ssriOpts ( opts )
338
347
if ( obj . algorithm && obj . digest ) {
339
348
return Hash . prototype . toString . call ( obj , opts )
340
349
} else if ( typeof obj === 'string' ) {
@@ -346,8 +355,7 @@ function stringify (obj, opts) {
346
355
347
356
module . exports . fromHex = fromHex
348
357
function fromHex ( hexDigest , algorithm , opts ) {
349
- opts = ssriOpts ( opts )
350
- const optString = getOptString ( opts . options )
358
+ const optString = getOptString ( opts ?. options )
351
359
return parse (
352
360
`${ algorithm } -${
353
361
Buffer . from ( hexDigest , 'hex' ) . toString ( 'base64' )
@@ -357,9 +365,8 @@ function fromHex (hexDigest, algorithm, opts) {
357
365
358
366
module . exports . fromData = fromData
359
367
function 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 )
363
370
return algorithms . reduce ( ( acc , algo ) => {
364
371
const digest = crypto . createHash ( algo ) . update ( data ) . digest ( 'base64' )
365
372
const hash = new Hash (
@@ -382,7 +389,6 @@ function fromData (data, opts) {
382
389
383
390
module . exports . fromStream = fromStream
384
391
function fromStream ( stream , opts ) {
385
- opts = ssriOpts ( opts )
386
392
const istream = integrityStream ( opts )
387
393
return new Promise ( ( resolve , reject ) => {
388
394
stream . pipe ( istream )
@@ -399,10 +405,9 @@ function fromStream (stream, opts) {
399
405
400
406
module . exports . checkData = checkData
401
407
function checkData ( data , sri , opts ) {
402
- opts = ssriOpts ( opts )
403
408
sri = parse ( sri , opts )
404
409
if ( ! sri || ! Object . keys ( sri ) . length ) {
405
- if ( opts . error ) {
410
+ if ( opts ? .error ) {
406
411
throw Object . assign (
407
412
new Error ( 'No valid integrity hashes to check against' ) , {
408
413
code : 'EINTEGRITY' ,
@@ -416,7 +421,8 @@ function checkData (data, sri, opts) {
416
421
const digest = crypto . createHash ( algorithm ) . update ( data ) . digest ( 'base64' )
417
422
const newSri = parse ( { algorithm, digest } )
418
423
const match = newSri . match ( sri , opts )
419
- if ( match || ! opts . error ) {
424
+ opts = opts || { }
425
+ if ( match || ! ( opts . error ) ) {
420
426
return match
421
427
} else if ( typeof opts . size === 'number' && ( data . length !== opts . size ) ) {
422
428
/* eslint-disable-next-line max-len */
@@ -440,7 +446,7 @@ function checkData (data, sri, opts) {
440
446
441
447
module . exports . checkStream = checkStream
442
448
function checkStream ( stream , sri , opts ) {
443
- opts = ssriOpts ( opts )
449
+ opts = opts || Object . create ( null )
444
450
opts . integrity = sri
445
451
sri = parse ( sri , opts )
446
452
if ( ! sri || ! Object . keys ( sri ) . length ) {
@@ -465,15 +471,14 @@ function checkStream (stream, sri, opts) {
465
471
}
466
472
467
473
module . exports . integrityStream = integrityStream
468
- function integrityStream ( opts = { } ) {
474
+ function integrityStream ( opts = Object . create ( null ) ) {
469
475
return new IntegrityStream ( opts )
470
476
}
471
477
472
478
module . exports . create = createIntegrity
473
479
function 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 )
477
482
478
483
const hashes = algorithms . map ( crypto . createHash )
479
484
0 commit comments