-
Notifications
You must be signed in to change notification settings - Fork 30
/
crypto.js
365 lines (315 loc) · 9.97 KB
/
crypto.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
'use strict'
const asn = require('asn1.js')
const {
createHmac,
createVerify,
createSign,
timingSafeEqual,
createPublicKey,
constants: {
RSA_PKCS1_PSS_PADDING,
RSA_PSS_SALTLEN_DIGEST,
RSA_PKCS1_PADDING,
RSA_PSS_SALTLEN_MAX_SIGN,
RSA_PSS_SALTLEN_AUTO
}
} = require('node:crypto')
let { sign: directSign, verify: directVerify } = require('node:crypto')
const { joseToDer, derToJose } = require('ecdsa-sig-formatter')
const Cache = require('mnemonist/lru-cache')
const { TokenError } = require('./error')
const useNewCrypto = typeof directSign === 'function'
const base64UrlMatcher = /[=+/]/g
const encoderMap = { '=': '', '+': '-', '/': '_' }
const privateKeyPemMatcher = /^-----BEGIN(?: (RSA|EC|ENCRYPTED))? PRIVATE KEY-----/
const publicKeyPemMatcher = /^-----BEGIN(?: (RSA))? PUBLIC KEY-----/
const publicKeyX509CertMatcher = '-----BEGIN CERTIFICATE-----'
const privateKeysCache = new Cache(1000)
const publicKeysCache = new Cache(1000)
const hsAlgorithms = ['HS256', 'HS384', 'HS512']
const esAlgorithms = ['ES256', 'ES384', 'ES512']
const rsaAlgorithms = ['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512']
const edAlgorithms = ['EdDSA']
const ecCurves = {
'1.2.840.10045.3.1.7': { bits: '256', names: ['P-256', 'prime256v1'] },
'1.3.132.0.10': { bits: '256', names: ['secp256k1'] },
'1.3.132.0.34': { bits: '384', names: ['P-384', 'secp384r1'] },
'1.3.132.0.35': { bits: '512', names: ['P-521', 'secp521r1'] }
}
/* istanbul ignore next */
if (!useNewCrypto) {
directSign = function (alg, data, options) {
if (typeof alg === 'undefined') {
throw new TokenError(TokenError.codes.signError, 'EdDSA algorithms are not supported by your Node.js version.')
}
return createSign(alg)
.update(data)
.sign(options)
}
}
const PrivateKey = asn.define('PrivateKey', function () {
this.seq().obj(
this.key('version').int(),
this.key('algorithm')
.seq()
.obj(
this.key('algorithm').objid(),
this.key('parameters')
.optional()
.objid()
)
)
})
const PublicKey = asn.define('PublicKey', function () {
this.seq().obj(
this.key('algorithm')
.seq()
.obj(
this.key('algorithm').objid(),
this.key('parameters')
.optional()
.objid()
)
)
})
const ECPrivateKey = asn.define('ECPrivateKey', function () {
this.seq().obj(
this.key('version').int(),
this.key('privateKey').octstr(),
this.key('parameters')
.explicit(0)
.optional()
.choice({ namedCurve: this.objid() })
)
})
function base64UrlReplacer(c) {
return encoderMap[c]
}
function cacheSet(cache, key, value, error) {
cache.set(key, [value, error])
return value || error
}
function performDetectPrivateKeyAlgorithm(key) {
if (key.match(publicKeyPemMatcher) || key.includes(publicKeyX509CertMatcher)) {
throw new TokenError(TokenError.codes.invalidKey, 'Public keys are not supported for signing.')
}
const pemData = key.trim().match(privateKeyPemMatcher)
if (!pemData) {
return 'HS256'
}
let keyData
let oid
let curveId
switch (pemData[1]) {
case 'RSA': // pkcs1 format - Can only be RSA key
return 'RS256'
case 'EC': // sec1 format - Can only be a EC key
keyData = ECPrivateKey.decode(key, 'pem', { label: 'EC PRIVATE KEY' })
curveId = keyData.parameters.value.join('.')
break
case 'ENCRYPTED': // Can be either RSA or EC key - we'll used the supplied algorithm
return 'ENCRYPTED'
default:
// pkcs8
keyData = PrivateKey.decode(key, 'pem', { label: 'PRIVATE KEY' })
oid = keyData.algorithm.algorithm.join('.')
switch (oid) {
case '1.2.840.113549.1.1.1': // RSA
return 'RS256'
case '1.2.840.10045.2.1': // EC
curveId = keyData.algorithm.parameters.join('.')
break
case '1.3.101.112': // Ed25519
case '1.3.101.113': // Ed448
return 'EdDSA'
default:
throw new TokenError(TokenError.codes.invalidKey, `Unsupported PEM PCKS8 private key with OID ${oid}.`)
}
}
const curve = ecCurves[curveId]
if (!curve) {
throw new TokenError(TokenError.codes.invalidKey, `Unsupported EC private key with curve ${curveId}.`)
}
return `ES${curve.bits}`
}
function performDetectPublicKeyAlgorithms(key) {
const publicKeyPemMatch = key.match(publicKeyPemMatcher)
if (key.match(privateKeyPemMatcher)) {
throw new TokenError(TokenError.codes.invalidKey, 'Private keys are not supported for verifying.')
} else if (publicKeyPemMatch && publicKeyPemMatch[1] === 'RSA') {
// pkcs1 format - Can only be RSA key
return rsaAlgorithms
} else if (!publicKeyPemMatch && !key.includes(publicKeyX509CertMatcher)) {
// Not a PEM, assume a plain secret
return hsAlgorithms
}
// if the key is a X509 cert we need to convert it
if (key.includes(publicKeyX509CertMatcher)) {
key = createPublicKey(key).export({ type: 'spki', format: 'pem' })
}
const keyData = PublicKey.decode(key, 'pem', { label: 'PUBLIC KEY' })
const oid = keyData.algorithm.algorithm.join('.')
let curveId
switch (oid) {
case '1.2.840.113549.1.1.1': // RSA
return rsaAlgorithms
case '1.2.840.10045.2.1': // EC
curveId = keyData.algorithm.parameters.join('.')
break
case '1.3.101.112': // Ed25519
case '1.3.101.113': // Ed448
return ['EdDSA']
default:
throw new TokenError(TokenError.codes.invalidKey, `Unsupported PEM PCKS8 public key with OID ${oid}.`)
}
const curve = ecCurves[curveId]
if (!curve) {
throw new TokenError(TokenError.codes.invalidKey, `Unsupported EC public key with curve ${curveId}.`)
}
return [`ES${curve.bits}`]
}
function detectPrivateKeyAlgorithm(key, providedAlgorithm) {
if (key instanceof Buffer) {
key = key.toString('utf-8')
} else if (typeof key !== 'string') {
throw new TokenError(TokenError.codes.invalidKey, 'The private key must be a string or a buffer.')
}
// Check cache first
const [cached, error] = privateKeysCache.get(key) || []
if (cached) {
return cached
} else if (error) {
throw error
}
// Try detecting
try {
const detectedAlgorithm = performDetectPrivateKeyAlgorithm(key)
if (detectedAlgorithm === 'ENCRYPTED') {
return cacheSet(privateKeysCache, key, providedAlgorithm)
}
return cacheSet(privateKeysCache, key, detectedAlgorithm)
} catch (e) {
throw cacheSet(privateKeysCache, key, null, TokenError.wrap(e, TokenError.codes.invalidKey, 'Unsupported PEM private key.'))
}
}
function detectPublicKeyAlgorithms(key) {
if (!key) {
return 'none'
}
// Check cache first
const [cached, error] = publicKeysCache.get(key) || []
if (cached) {
return cached
} else if (error) {
throw error
}
// Try detecting
try {
if (key instanceof Buffer) {
key = key.toString('utf-8')
} else if (typeof key !== 'string') {
throw new TokenError(TokenError.codes.invalidKey, 'The public key must be a string or a buffer.')
}
return cacheSet(publicKeysCache, key, performDetectPublicKeyAlgorithms(key))
} catch (e) {
throw cacheSet(
publicKeysCache,
key,
null,
TokenError.wrap(e, TokenError.codes.invalidKey, 'Unsupported PEM public key.')
)
}
}
function createSignature(algorithm, key, input) {
try {
const type = algorithm.slice(0, 2)
const alg = `sha${algorithm.slice(2)}`
let raw
let options
switch (type) {
case 'HS':
raw = createHmac(alg, key)
.update(input)
.digest('base64')
break
case 'ES':
raw = derToJose(directSign(alg, Buffer.from(input, 'utf-8'), key), algorithm).toString('base64')
break
case 'RS':
case 'PS':
options = {
key,
padding: RSA_PKCS1_PADDING,
saltLength: RSA_PSS_SALTLEN_MAX_SIGN
}
if (type === 'PS') {
options.padding = RSA_PKCS1_PSS_PADDING
options.saltLength = RSA_PSS_SALTLEN_DIGEST
}
raw = createSign(alg)
.update(input)
.sign(options)
.toString('base64')
break
case 'Ed':
raw = directSign(undefined, Buffer.from(input, 'utf-8'), key).toString('base64')
}
return raw.replace(base64UrlMatcher, base64UrlReplacer)
} catch (e) {
/* istanbul ignore next */
throw new TokenError(TokenError.codes.signError, 'Cannot create the signature.', { originalError: e })
}
}
function verifySignature(algorithm, key, input, signature) {
try {
const type = algorithm.slice(0, 2)
const alg = `SHA${algorithm.slice(2)}`
signature = Buffer.from(signature, 'base64')
if (type === 'HS') {
try {
return timingSafeEqual(
createHmac(alg, key)
.update(input)
.digest(),
signature
)
} catch (e) {
return false
}
} else if (type === 'Ed') {
// Check if supported on Node 10
/* istanbul ignore next */
if (typeof directVerify === 'function') {
return directVerify(undefined, Buffer.from(input, 'utf-8'), key, signature)
} else {
throw new TokenError(TokenError.codes.signError, 'EdDSA algorithms are not supported by your Node.js version.')
}
}
const options = { key, padding: RSA_PKCS1_PADDING, saltLength: RSA_PSS_SALTLEN_AUTO }
if (type === 'PS') {
options.padding = RSA_PKCS1_PSS_PADDING
options.saltLength = RSA_PSS_SALTLEN_DIGEST
} else if (type === 'ES') {
signature = joseToDer(signature, algorithm)
}
return createVerify('RSA-' + alg)
.update(input)
.verify(options, signature)
} catch (e) {
/* istanbul ignore next */
throw new TokenError(TokenError.codes.verifyError, 'Cannot verify the signature.', { originalError: e })
}
}
module.exports = {
useNewCrypto,
base64UrlMatcher,
base64UrlReplacer,
hsAlgorithms,
rsaAlgorithms,
esAlgorithms,
edAlgorithms,
detectPrivateKeyAlgorithm,
detectPublicKeyAlgorithms,
createSignature,
verifySignature
}