Skip to content

Commit 91c3ad6

Browse files
committed
refactor: use async node:crypto methods when available
1 parent 2bf20ea commit 91c3ad6

19 files changed

+496
-235
lines changed

decode.js

+46-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,52 @@
1-
var jws = require('jws');
1+
function payloadFromJWS(encodedPayload, encoding = "utf8") {
2+
try {
3+
return Buffer.from(encodedPayload, "base64").toString(encoding);
4+
} catch (_) {
5+
return;
6+
}
7+
}
8+
9+
function headerFromJWS(encodedHeader) {
10+
try {
11+
return JSON.parse(Buffer.from(encodedHeader, "base64").toString());
12+
} catch (_) {
13+
return;
14+
}
15+
}
16+
17+
const JWS_REGEX = /^[a-zA-Z0-9\-_]+?\.[a-zA-Z0-9\-_]+?\.([a-zA-Z0-9\-_]+)?$/;
18+
19+
function isValidJws(string) {
20+
return JWS_REGEX.test(string);
21+
}
22+
23+
function jwsDecode(token, opts) {
24+
opts = opts || {};
25+
26+
if (!isValidJws(token)) return null;
27+
28+
let [header, payload, signature] = token.split('.');
29+
30+
header = headerFromJWS(header);
31+
if (header === undefined) return null;
32+
33+
payload = payloadFromJWS(payload);
34+
if (payload === undefined) return null;
35+
36+
if (header.typ === "JWT" || opts.json){
37+
payload = JSON.parse(payload);
38+
}
39+
40+
return {
41+
header,
42+
payload,
43+
signature,
44+
};
45+
}
246

347
module.exports = function (jwt, options) {
448
options = options || {};
5-
var decoded = jws.decode(jwt, options);
49+
const decoded = jwsDecode(jwt, options);
650
if (!decoded) { return null; }
751
var payload = decoded.payload;
852

lib/asymmetricKeyDetailsSupported.js

-3
This file was deleted.

lib/base64url.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/* istanbul ignore file */
2+
if (Buffer.isEncoding("base64url")) {
3+
module.exports = (buf) => buf.toString("base64url");
4+
} else {
5+
const fromBase64 = (base64) =>
6+
// eslint-disable-next-line no-div-regex
7+
base64.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
8+
module.exports = (buf) => fromBase64(buf.toString("base64"));
9+
}

lib/flags.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/* istanbul ignore file */
2+
const [major, minor] = process.versions.node.split('.').map((v) => parseInt(v, 10));
3+
4+
module.exports.RSA_PSS_KEY_DETAILS_SUPPORTED = major > 16 || (major === 16 && minor >= 9);
5+
module.exports.ASYMMETRIC_KEY_DETAILS_SUPPORTED = major > 15 || (major === 15 && minor >= 7);

lib/oneShotAlgs.js

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
const { constants } = require("crypto");
2+
3+
module.exports = function(alg, key) {
4+
switch (alg) {
5+
case 'RS256':
6+
return {
7+
digest: 'sha256',
8+
key: { key, padding: constants.RSA_PKCS1_PADDING },
9+
};
10+
case 'RS384':
11+
return {
12+
digest: 'sha384',
13+
key: { key, padding: constants.RSA_PKCS1_PADDING },
14+
};
15+
case 'RS512':
16+
return {
17+
digest: 'sha512',
18+
key: { key, padding: constants.RSA_PKCS1_PADDING },
19+
};
20+
case 'PS256':
21+
return {
22+
digest: 'sha256',
23+
key: { key, padding: constants.RSA_PKCS1_PSS_PADDING, saltLength: constants.RSA_PSS_SALTLEN_DIGEST },
24+
};
25+
case 'PS384':
26+
return {
27+
digest: 'sha384',
28+
key: { key, padding: constants.RSA_PKCS1_PSS_PADDING, saltLength: constants.RSA_PSS_SALTLEN_DIGEST },
29+
};
30+
case 'PS512':
31+
return {
32+
digest: 'sha512',
33+
key: { key, padding: constants.RSA_PKCS1_PSS_PADDING, saltLength: constants.RSA_PSS_SALTLEN_DIGEST },
34+
};
35+
case 'ES256':
36+
return {
37+
digest: 'sha256',
38+
key: { key, dsaEncoding: 'ieee-p1363' },
39+
};
40+
case 'ES384':
41+
return {
42+
digest: 'sha384',
43+
key: { key, dsaEncoding: 'ieee-p1363' },
44+
};
45+
case 'ES512':
46+
return {
47+
digest: 'sha512',
48+
key: { key, dsaEncoding: 'ieee-p1363' },
49+
};
50+
default:
51+
throw new Error('unreachable');
52+
}
53+
};

lib/psSupported.js

-3
This file was deleted.

lib/rsaPssKeyDetailsSupported.js

-3
This file was deleted.

lib/validateAsymmetricKey.js

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
const ASYMMETRIC_KEY_DETAILS_SUPPORTED = require('./asymmetricKeyDetailsSupported');
2-
const RSA_PSS_KEY_DETAILS_SUPPORTED = require('./rsaPssKeyDetailsSupported');
1+
const { ASYMMETRIC_KEY_DETAILS_SUPPORTED, RSA_PSS_KEY_DETAILS_SUPPORTED } = require('./flags');
32

43
const allowedAlgorithmsForKeys = {
54
'ec': ['ES256', 'ES384', 'ES512'],
@@ -52,7 +51,7 @@ module.exports = function(algorithm, key) {
5251
const length = parseInt(algorithm.slice(-3), 10);
5352
const { hashAlgorithm, mgf1HashAlgorithm, saltLength } = key.asymmetricKeyDetails;
5453

55-
if (hashAlgorithm !== `sha${length}` || mgf1HashAlgorithm !== hashAlgorithm) {
54+
if (hashAlgorithm !== undefined && (hashAlgorithm !== `sha${length}` || mgf1HashAlgorithm !== hashAlgorithm)) {
5655
throw new Error(`Invalid key for this operation, its RSA-PSS parameters do not meet the requirements of "alg" ${algorithm}.`);
5756
}
5857

package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -36,23 +36,23 @@
3636
"url": "https://github.com/auth0/node-jsonwebtoken/issues"
3737
},
3838
"dependencies": {
39-
"jws": "^3.2.2",
4039
"lodash.includes": "^4.3.0",
4140
"lodash.isboolean": "^3.0.3",
4241
"lodash.isinteger": "^4.0.4",
4342
"lodash.isnumber": "^3.0.3",
4443
"lodash.isplainobject": "^4.0.6",
4544
"lodash.isstring": "^4.0.1",
4645
"lodash.once": "^4.0.0",
47-
"ms": "^2.1.1",
48-
"semver": "^7.5.4"
46+
"ms": "^2.1.1"
4947
},
5048
"devDependencies": {
5149
"atob": "^2.1.2",
5250
"chai": "^4.1.2",
5351
"conventional-changelog": "~1.1.0",
5452
"cost-of-modules": "^1.0.1",
5553
"eslint": "^4.19.1",
54+
"jose": "^5.8.0",
55+
"jws": "^3.2.2",
5656
"mocha": "^5.2.0",
5757
"nsp": "^2.6.2",
5858
"nyc": "^11.9.0",

sign.js

+88-44
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
const timespan = require('./lib/timespan');
2-
const PS_SUPPORTED = require('./lib/psSupported');
32
const validateAsymmetricKey = require('./lib/validateAsymmetricKey');
4-
const jws = require('jws');
53
const includes = require('lodash.includes');
64
const isBoolean = require('lodash.isboolean');
75
const isInteger = require('lodash.isinteger');
86
const isNumber = require('lodash.isnumber');
97
const isPlainObject = require('lodash.isplainobject');
108
const isString = require('lodash.isstring');
11-
const once = require('lodash.once');
12-
const { KeyObject, createSecretKey, createPrivateKey } = require('crypto')
9+
const crypto = require('crypto')
10+
const oneShotAlgs = require('./lib/oneShotAlgs');
11+
const encodeBase64url = require('./lib/base64url');
1312

14-
const SUPPORTED_ALGS = ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512', 'none'];
15-
if (PS_SUPPORTED) {
16-
SUPPORTED_ALGS.splice(3, 0, 'PS256', 'PS384', 'PS512');
17-
}
13+
const SUPPORTED_ALGS = [
14+
'RS256', 'RS384', 'RS512',
15+
'PS256', 'PS384', 'PS512',
16+
'ES256', 'ES384', 'ES512',
17+
'HS256', 'HS384', 'HS512',
18+
'none',
19+
];
1820

1921
const sign_options_schema = {
2022
expiresIn: { isValid: function(value) { return isInteger(value) || (isString(value) && value); }, message: '"expiresIn" should be a number of seconds or string representing a timespan' },
@@ -39,6 +41,7 @@ const registered_claims_schema = {
3941
nbf: { isValid: isNumber, message: '"nbf" should be a number of seconds' }
4042
};
4143

44+
4245
function validate(schema, allowUnknown, object, parameterName) {
4346
if (!isPlainObject(object)) {
4447
throw new Error('Expected "' + parameterName + '" to be a plain object.');
@@ -83,14 +86,41 @@ const options_for_objects = [
8386
'jwtid',
8487
];
8588

86-
module.exports = function (payload, secretOrPrivateKey, options, callback) {
89+
function encodePayload(payload, encoding = 'utf8') {
90+
let buf;
91+
if (payload instanceof Uint8Array) {
92+
buf = Buffer.from(payload)
93+
} else if (typeof payload === 'string') {
94+
buf = Buffer.from(payload, encoding);
95+
} else {
96+
buf = Buffer.from(JSON.stringify(payload), encoding);
97+
}
98+
99+
return encodeBase64url(buf);
100+
}
101+
102+
function encodeHeader(header) {
103+
return encodeBase64url(Buffer.from(JSON.stringify(header)));
104+
}
105+
106+
module.exports = function(payload, secretOrPrivateKey, options, callback) {
87107
if (typeof options === 'function') {
88108
callback = options;
89109
options = {};
90110
} else {
91111
options = options || {};
92112
}
93113

114+
let done;
115+
if (callback) {
116+
done = callback;
117+
} else {
118+
done = function(err, data) {
119+
if (err) throw err;
120+
return data;
121+
};
122+
}
123+
94124
const isObjectPayload = typeof payload === 'object' &&
95125
!Buffer.isBuffer(payload);
96126

@@ -101,8 +131,8 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) {
101131
}, options.header);
102132

103133
function failure(err) {
104-
if (callback) {
105-
return callback(err);
134+
if (done) {
135+
return done(err);
106136
}
107137
throw err;
108138
}
@@ -111,12 +141,12 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) {
111141
return failure(new Error('secretOrPrivateKey must have a value'));
112142
}
113143

114-
if (secretOrPrivateKey != null && !(secretOrPrivateKey instanceof KeyObject)) {
144+
if (secretOrPrivateKey != null && !(secretOrPrivateKey instanceof crypto.KeyObject)) {
115145
try {
116-
secretOrPrivateKey = createPrivateKey(secretOrPrivateKey)
146+
secretOrPrivateKey = crypto.createPrivateKey(secretOrPrivateKey)
117147
} catch (_) {
118148
try {
119-
secretOrPrivateKey = createSecretKey(typeof secretOrPrivateKey === 'string' ? Buffer.from(secretOrPrivateKey) : secretOrPrivateKey)
149+
secretOrPrivateKey = crypto.createSecretKey(typeof secretOrPrivateKey === 'string' ? Buffer.from(secretOrPrivateKey) : secretOrPrivateKey)
120150
} catch (_) {
121151
return failure(new Error('secretOrPrivateKey is not valid key material'));
122152
}
@@ -129,12 +159,6 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) {
129159
if (secretOrPrivateKey.type !== 'private') {
130160
return failure(new Error((`secretOrPrivateKey must be an asymmetric key when using ${header.alg}`)))
131161
}
132-
if (!options.allowInsecureKeySizes &&
133-
!header.alg.startsWith('ES') &&
134-
secretOrPrivateKey.asymmetricKeyDetails !== undefined && //KeyObject.asymmetricKeyDetails is supported in Node 15+
135-
secretOrPrivateKey.asymmetricKeyDetails.modulusLength < 2048) {
136-
return failure(new Error(`secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg}`));
137-
}
138162
}
139163

140164
if (typeof payload === 'undefined') {
@@ -224,30 +248,50 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) {
224248
}
225249
});
226250

227-
const encoding = options.encoding || 'utf8';
228-
229-
if (typeof callback === 'function') {
230-
callback = callback && once(callback);
231-
232-
jws.createSign({
233-
header: header,
234-
privateKey: secretOrPrivateKey,
235-
payload: payload,
236-
encoding: encoding
237-
}).once('error', callback)
238-
.once('done', function (signature) {
239-
// TODO: Remove in favor of the modulus length check before signing once node 15+ is the minimum supported version
240-
if(!options.allowInsecureKeySizes && /^(?:RS|PS)/.test(header.alg) && signature.length < 256) {
241-
return callback(new Error(`secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg}`))
242-
}
243-
callback(null, signature);
244-
});
245-
} else {
246-
let signature = jws.sign({header: header, payload: payload, secret: secretOrPrivateKey, encoding: encoding});
247-
// TODO: Remove in favor of the modulus length check before signing once node 15+ is the minimum supported version
248-
if(!options.allowInsecureKeySizes && /^(?:RS|PS)/.test(header.alg) && signature.length < 256) {
249-
throw new Error(`secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg}`)
251+
const sync = done !== callback || header.alg === 'none' || header.alg.startsWith('HS') || parseInt(process.versions.node, 10) < 16;
252+
const data = Buffer.from(`${encodeHeader(header)}.${encodePayload(payload, options.encoding)}`);
253+
254+
if (header.alg === 'none') {
255+
return done(null, `${data}.`)
256+
}
257+
258+
if (header.alg.startsWith('HS')) {
259+
const signature = encodeBase64url(crypto.createHmac(`sha${header.alg.substring(2, 5)}`, secretOrPrivateKey).update(data).digest());
260+
return done(null, `${data}.${signature}`);
261+
}
262+
263+
if (sync) {
264+
const { digest, key } = oneShotAlgs(header.alg, secretOrPrivateKey);
265+
266+
let signature;
267+
try {
268+
signature = crypto.sign(digest, data, key);
269+
} catch (err) {
270+
return done(err);
250271
}
251-
return signature
272+
273+
if(!options.allowInsecureKeySizes && (header.alg.startsWith('RS') || header.alg.startsWith('PS')) && signature.byteLength < 256) {
274+
return done(new Error(`secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg}`));
275+
}
276+
277+
const token = `${data}.${encodeBase64url(signature)}`;
278+
279+
return done(null, token);
252280
}
281+
282+
const { digest, key } = oneShotAlgs(header.alg, secretOrPrivateKey);
283+
284+
crypto.sign(digest, data, key, (err, signature) => {
285+
if (err) {
286+
return done(err);
287+
}
288+
289+
if(!options.allowInsecureKeySizes && (header.alg.startsWith('RS') || header.alg.startsWith('PS')) && signature.byteLength < 256) {
290+
return done(new Error(`secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg}`));
291+
}
292+
293+
const token = `${data}.${encodeBase64url(signature)}`;
294+
295+
return done(null, token);
296+
});
253297
};

0 commit comments

Comments
 (0)