// // used by jws // Source: https://github.com/auth0/node-jwa // var bufferEqual = require('./buffer-equal-constant-time'); var Buffer = require('safe-buffer').Buffer; var crypto = require('crypto'); var formatEcdsa = require('./ecdsa-sig-formatter'); var util = require('util'); var MSG_INVALID_ALGORITHM = '"%s" is not a valid algorithm.\n Supported algorithms are:\n "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "PS256", "PS384", "PS512", "ES256", "ES384", "ES512" and "none".' var MSG_INVALID_SECRET = 'secret must be a string or buffer'; var MSG_INVALID_VERIFIER_KEY = 'key must be a string or a buffer'; var MSG_INVALID_SIGNER_KEY = 'key must be a string, a buffer or an object'; var supportsKeyObjects = typeof crypto.createPublicKey === 'function'; if (supportsKeyObjects) { MSG_INVALID_VERIFIER_KEY += ' or a KeyObject'; MSG_INVALID_SECRET += 'or a KeyObject'; } function checkIsPublicKey(key) { if (Buffer.isBuffer(key)) { return; } if (typeof key === 'string') { return; } if (!supportsKeyObjects) { throw typeError(MSG_INVALID_VERIFIER_KEY); } if (typeof key !== 'object') { throw typeError(MSG_INVALID_VERIFIER_KEY); } if (typeof key.type !== 'string') { throw typeError(MSG_INVALID_VERIFIER_KEY); } if (typeof key.asymmetricKeyType !== 'string') { throw typeError(MSG_INVALID_VERIFIER_KEY); } if (typeof key.export !== 'function') { throw typeError(MSG_INVALID_VERIFIER_KEY); } }; function checkIsPrivateKey(key) { if (Buffer.isBuffer(key)) { return; } if (typeof key === 'string') { return; } if (typeof key === 'object') { return; } throw typeError(MSG_INVALID_SIGNER_KEY); }; function checkIsSecretKey(key) { if (Buffer.isBuffer(key)) { return; } if (typeof key === 'string') { return key; } if (!supportsKeyObjects) { throw typeError(MSG_INVALID_SECRET); } if (typeof key !== 'object') { throw typeError(MSG_INVALID_SECRET); } if (key.type !== 'secret') { throw typeError(MSG_INVALID_SECRET); } if (typeof key.export !== 'function') { throw typeError(MSG_INVALID_SECRET); } } function fromBase64(base64) { return base64 .replace(/=/g, '') .replace(/\+/g, '-') .replace(/\//g, '_'); } function toBase64(base64url) { base64url = base64url.toString(); var padding = 4 - base64url.length % 4; if (padding !== 4) { for (var i = 0; i < padding; ++i) { base64url += '='; } } return base64url .replace(/\-/g, '+') .replace(/_/g, '/'); } function typeError(template) { var args = [].slice.call(arguments, 1); var errMsg = util.format.bind(util, template).apply(null, args); return new TypeError(errMsg); } function bufferOrString(obj) { return Buffer.isBuffer(obj) || typeof obj === 'string'; } function normalizeInput(thing) { if (!bufferOrString(thing)) thing = JSON.stringify(thing); return thing; } function createHmacSigner(bits) { return function sign(thing, secret) { checkIsSecretKey(secret); thing = normalizeInput(thing); var hmac = crypto.createHmac('sha' + bits, secret); var sig = (hmac.update(thing), hmac.digest('base64')) return fromBase64(sig); } } function createHmacVerifier(bits) { return function verify(thing, signature, secret) { var computedSig = createHmacSigner(bits)(thing, secret); return bufferEqual(Buffer.from(signature), Buffer.from(computedSig)); } } function createKeySigner(bits) { return function sign(thing, privateKey) { checkIsPrivateKey(privateKey); thing = normalizeInput(thing); // Even though we are specifying "RSA" here, this works with ECDSA // keys as well. var signer = crypto.createSign('RSA-SHA' + bits); var sig = (signer.update(thing), signer.sign(privateKey, 'base64')); return fromBase64(sig); } } function createKeyVerifier(bits) { return function verify(thing, signature, publicKey) { checkIsPublicKey(publicKey); thing = normalizeInput(thing); signature = toBase64(signature); var verifier = crypto.createVerify('RSA-SHA' + bits); verifier.update(thing); return verifier.verify(publicKey, signature, 'base64'); } } function createPSSKeySigner(bits) { return function sign(thing, privateKey) { checkIsPrivateKey(privateKey); thing = normalizeInput(thing); var signer = crypto.createSign('RSA-SHA' + bits); var sig = (signer.update(thing), signer.sign({ key: privateKey, padding: crypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST }, 'base64')); return fromBase64(sig); } } function createPSSKeyVerifier(bits) { return function verify(thing, signature, publicKey) { checkIsPublicKey(publicKey); thing = normalizeInput(thing); signature = toBase64(signature); var verifier = crypto.createVerify('RSA-SHA' + bits); verifier.update(thing); return verifier.verify({ key: publicKey, padding: crypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST }, signature, 'base64'); } } function createECDSASigner(bits) { var inner = createKeySigner(bits); return function sign() { var signature = inner.apply(null, arguments); signature = formatEcdsa.derToJose(signature, 'ES' + bits); return signature; }; } function createECDSAVerifer(bits) { var inner = createKeyVerifier(bits); return function verify(thing, signature, publicKey) { signature = formatEcdsa.joseToDer(signature, 'ES' + bits).toString('base64'); var result = inner(thing, signature, publicKey); return result; }; } function createNoneSigner() { return function sign() { return ''; } } function createNoneVerifier() { return function verify(thing, signature) { return signature === ''; } } module.exports = function jwa(algorithm) { var signerFactories = { hs: createHmacSigner, rs: createKeySigner, ps: createPSSKeySigner, es: createECDSASigner, none: createNoneSigner, } var verifierFactories = { hs: createHmacVerifier, rs: createKeyVerifier, ps: createPSSKeyVerifier, es: createECDSAVerifer, none: createNoneVerifier, } var match = algorithm.match(/^(RS|PS|ES|HS)(256|384|512)$|^(none)$/); if (!match) throw typeError(MSG_INVALID_ALGORITHM, algorithm); var algo = (match[1] || match[3]).toLowerCase(); var bits = match[2]; return { sign: signerFactories[algo](bits), verify: verifierFactories[algo](bits), } };